[关闭]
@jsongao98 2021-04-24T12:13:45.000000Z 字数 10336 阅读 53

函数

JavaScript


参数

  • 未被提供的参数默认值为undefined
  • 参数默认值使用“=”,“=”后可接表达式
  • ||、?? 、&&操作符,注意优先级使用()
  • 使用rest语法:“...”,即扩展操作符收集参数,为按值传递的参数数组,可以使用数组方法。
  • 使用arguments类数组对象(index,length)来访问参数,可以按索引访问参数,但无数组方法可用。
    • Array.prototype.slice.call(arguments,0);
    • [].slice.call(arguments,0);
    • 可通过以上两种方法转为真正数组

全局对象

在浏览器中全局对象为window对象,在nodejs中名字为global。新标准中,全局对象通用名为globalThis。
在浏览器中,除非我们使用 modules,否则使用 var 声明的全局函数和变量会成为全局对象的属性。
使用polyfill为旧版浏览器添加现代语言功能

  1. if (!window.Promise) {
  2. window.Promise = ... // 定制实现现代语言功能
  3. }

插一句题外话,基本数据类型的打印console.log()是不会有什么问题的,但是对于对象(引用数据类型)可靠的方法是通过打断点的方式去看(console.log在浏览器中异步方式进行)

JavaScript中函数没有重载

什么是重载:
函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。

为什么没有重载:
js中函数实际是对象,函数名是指针,所以如果出现同名函数,则后定义的函数会覆盖先定义的函数的引用变量(函数名,指针)。

1.函数声明、函数表达式

函数声明是一个代码块,函数表达式是一个赋值语句;代码块不需;结尾,赋值语句需要。只有函数表达式才可以立即执行。
全局 函数声明会在脚本执行之前创建,函数表达式只有代码执行到才会被创建(声明提升),同理,函数声明在该声明的块作用域内随处可用。
严格模式 下,当一个函数声明在一个代码块内时,它在该代码块内的任何位置都是可见的。但在代码块外不可见。
解决无法在代码块外调用代码块内函数的问题
使用函数表达式,赋值给代码块外的变量


2.命名函数表达式(NFE)

  1. let sayHi = function func(){...};//func即为带名字的函数表达式
  2. //关于名字func: 1.允许在函数内部引用自己(用处: 即使变量被修改了也可以使用func进行自身嵌套,递归调用) // 2.在函数外部不可见

3.new Function语法

使用 new Function 创建一个函数,那么该函数的 [[Environment]] 并不指向当前的词法环境,而是指向全局环境。

  1. let func = new Function ([arg1, arg2, ...argN], functionBody);//基本语法
  2. ----------------------------------------------------------------------------------------------------------
  3. <!--以下三种语法含义都相同-->
  4. let sum = new Function('a', 'b', 'return a + b'); // 基础语法
  5. let sum = new Function('a,b', 'return a + b'); // 逗号分隔
  6. let sum = new Function('a , b', 'return a + b'); // 逗号和空格分隔
  7. alert( sum(1, 2) ); // 3
  8. ----------------------------------------------------------------------------------------------------------
  9. let str = ... 动态地接收来自服务器的代码 ...
  10. let func = new Function(str);
  11. func();

4.构造函数

new 在执行时会做四件事情:

  ① 在内存中创建一个新的空对象。

  ② 让 this 指向这个新的对象。

  ③ 执行构造函数里面的代码,给这个新对象添加属性和方法。

  ④ 返回这个新对象(所以构造函数里面不需要 return )。

  • 静态成员:在构造函数本上添加的成员称为静态成员,只能由构造函数本身来访问
  • 实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问(一般通过this来指向实例对象,实例对象还可以访问函数的prototype)

5.匿名函数、箭头函数

箭头函数没有自身的this,也没有arguments类数组对象,所以在箭头函数中访问arguments对象,所访问的是箭头函数外部的函数arguments对象。箭头函数中的this隐式绑定了其父级作用域。


变量、变量生命周期、JS数据类型与堆栈

  • 区分:
    • var:
      • 存在,因为声明提升, 不报错显示undefined; 但未初始化(无法使用,即暂时性死区)
      • 全局的 var 变量其实仅仅是为 global 对象添加了一条属性。
      • 只有全局作用域和函数作用域。没法被{}块级作用域限制。
    • let、const:
      • 不存在,因为无提升;
      • 不能重复声明,所以配合for循环
      • 可以声明块级作用域的变量
      • const必须在声明时初始化。
  • 变量
    • 局部变量
      • 在函数中声明,且在函数返回后不会被其他作用域所使用的对象。
      • 生命周期:对于函数内的局部变量,一般情况会随之函数调用的结束而不再占用栈内存。
    • 全局变量
      • 全局变量就是 global,在浏览器上为window在node里为global。全局变量会被默认添加到函数作用域链的最低端
      • 生命周期:全局变量的生存周期是永久的,除非我们主动销毁
  • 变量存在哪?
    • 一般情况,基础数据类型存储在栈内存中,引用类型存储在堆内存中。
    • 考虑闭包,在函数返回(不存在于栈内存了)后仍有未执行作用域(函数或是类)使用到外层函数中变量,为了保证变量不被销毁,在堆中先生成一个对象就叫 Scope 吧,把变量作为 Scope 的属性给存起来(所以现在堆内存有两块存一样值变量的内存,但堆内存首地址不同)
    • 递归调用,此时都独立存在于栈中,每一次调用从堆内存中拿数据,改变的只是栈中的数据。
  • 严格模式
    • 在严格模式下, 变量必须先声明,直接给变量赋值,不会隐式创建全局变量
    • 非严格,函数作用域中有这么一个语句var a = b = 1,b会隐式成为全局变量,而a只能在函数作用域内被访问。最好是通过var a = 1,b = 1;

参数传递

参数传递其实有三种:按值传递、按引用传递、按共享传递(call by sharing)

准确的说,JS中的基本类型按值传递,对象(引用)类型按共享传递的

据按共享传递的求值策略,a和b是两个不同的引用(b是a的引用副本),但引用相同的值。
该策略的重点是:调用函数传参时,函数接受对象实参引用的副本(既不是按值传递的对象副本,也不是按引用传递的隐式引用)。 它和按引用传递的不同在于:在共享传递中对函数形参的赋值,不会影响实参的值。如下面例子中,不可以通过修改形参o的值,来修改obj的值。

  • 我对call by sharing的理解是:

  • Created with Raphaël 2.1.2实参实参形参形参函数函数形参继承自实参,实参中作为形参的prototype形参实例属性覆盖原型同名属性(直接赋值)通过.property访问形参属性,此时形参内无实例属性,访问的是原型,即实参属性
    1. var obj = {x : 1};
    2. function foo(obj){
    3. obj = { x = 100;}
    4. }
    5. foo(obj);
    6. console.log(obj.x); // 仍然是1, obj并未被修改为100.

  • 所以修改形参对象的属性值,也会影响到实参的属性值

    1. var obj = {x : 1};
    2. function foo(o) {
    3. o.x = 3;
    4. }
    5. foo(obj);
    6. console.log(obj.x); // 3, 被修改了!

    关于this的理解

    1.内存有栈内存和堆内存。原始数据类型存储在栈内存(执行栈),引用类型(obj、function、array..)存储在堆内存。
    2.词法环境:每个函数在创建的时候都会创建[[Environment]]隐藏属性,词法环境包括环境记录(参数、变量)和对外部词法环境的引用。

    3.this(对象)指针指向函数的执行上下文,我的理解是指向该函数在执行栈中的上一层对象。

    -函数被直接调用的时候,即通过函数名直接访问func(...),this指向执行栈中最底部的globalThis,在浏览器中为window对象,在nodejs中为global对象。所以在闭包中有了call、apply。

    -函数作为对象方法的时候:我的理解是
    情况一:此时如果对象方法(存在于堆内存)是作为参数被传递,即按共享传递(call by sharing),那么其方法中的this会丢失,并不会指向对象本身。还有像setInterval/setTimeout(obj.func,ms)这种也是,obj.func作为参数传递给调度程序。所以有了bind或者是包装函数wrapper(箭头函数中调用(直接引用)对象方法)。
    情况二:对象方法按引用传递,比如对象方法被直接调用的时候,此时this不会丢失。

    -对象方法中的闭包:
    this丢失,通过中间变量存储this。比如obj.func0().func1(),func1中的this不会指向obj而是指向obj之外的词法环境。


    函数对象

    函数内置属性name、length(rest并不算在length里)//区别arguments类数组对象的length属性,函数的length是指本身的参数个数,arguments的length是函数被调用时被传入的参数个数。


    属性不是变量
    被赋值给函数的属性,比如 sayHi.counter = 0,不会 在函数内定义一个局部变量 counter。换句话说,属性 counter 和变量 let counter 是毫不相关的两个东西。我们可以把函数当作对象,在它里面存储属性,但是这对它的执行没有任何影响。变量不是函数属性,反之亦然。它们之间是平行的。


    函数属性和闭包

    函数属性和变量的不同之处:

    • 存在外部函数中 (外部词法环境) 的变量,只能通过嵌套函数来访问 (闭包) ,在函数之外无法访问函数内的变量
    • 函数属性是直接绑定在函数上的,如下边例子中,属性count被直接存储在函数(counter())里,而不是它外部的词法环境( makeCounter())。所以在函数外部是可以访问到。
    1. function makeCounter() {
    2. function counter() {
    3. return counter.count++;
    4. };
    5. counter.count = 0; //自定义函数属性
    6. return counter;
    7. }
    8. let counter = makeCounter();
    9. counter.count = 10;// 函数属性可以从外部被访问,函数变量(函数作用域)不行
    10. alert( counter() ); // 10

    闭包是指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数被返回(寿命 终结)了之后。在 JavaScript 中,所有函数都是天生闭包的(只有一个例外,将在 "new Function" 语法 中讲到)。
    也就是说:JavaScript 中的函数会自动通过隐藏的[[Environment]]属性记住创建它们的位置(即指向函数创建时的词法环境)所以它们都可以访问外部变量。在面试时,前端开发者通常会被问到“什么是闭包?”,正确的回答应该是闭包的定义,并解释清楚为什么 JavaScript 中的所有函数都是闭包的,以及可能的关于 [[Environment]]属性和词法环境原理的技术细节。

    [[Environment]]是取决于创建!!而不是调用!!

    this指向才是取决于函数被调用时候的执行上下文


    1.
    闭包情况例子一,执行栈中推入box(),后再推入function().

    1. function box(){
    2. var arr = [];
    3. for(var i=0;i<5;i++){
    4. arr[i] = function(){
    5. return i;
    6. }();//每次循环立即执行闭包,若无立即执行,则结果为4,4,4,4,4
    7. }
    8. return arr;
    9. }
    10. alert(box());// 0,1,2,3,4

    2.
    闭包例子二,首先,object.getNameFunc()()就是代码中返回的那个匿名函数,其实就是闭包,如果getNameFunc()中直接使>用this,那this指向object是没有问题的,但是现在this是在返回的匿名函数中使用的,object.getNameFunc()()就相当于在全局环境中直接调用这个匿名函数!自然this指向window,我们只是被object这个前缀给迷惑了!

    1. var name = "The Window";
    2. var object = {
    3. name: "My object",
    4. getNameFunc: function() {
    5. return function() {
    6. return this.name;
    7. };
    8. }
    9. };
    10. alert(object.getNameFunc()()); // "The Window"

    为了防止this的指向与我们预期的不一样,在实际编码中,经常用到var that = this;来提前保存我们期望的作用域,然后用that.name获取对象的变量。

    1. var name = "The Window";
    2. var object = {
    3.     name : "My Object",
    4.     getNameFunc : function(){
    5.     
    6.       var that = this;//
    7.       
    8.       return function(){
    9.       return that.name;
    10.       };
    11.     }
    12. };
    13. alert(object.getNameFunc()());// "My Object"

    setTimeout和setInterval


    装饰器模式(封装)、call/apply/bind


    call/apply的区别:

    func.call(context, ...args); // 使用 spread 语法将数组作为列表传递
    func.apply(context, args); // 与使用 call 相同

    call 和 apply 之间唯一的语法区别是,call 期望一个参数列表(可迭代对象),而 apply 期望一个包含这些参数的类数组对象。


    call/apply方法中的this问题:

    func.call(this,args);
    首先this是指针,指向当前函数的执行上下文,即函数被调用时候的外部词法环境。call/apply将当前函数执行上下文(this)和参数传回给原方法(func)。并且call/apply中的this赋值给原方法func中的this,所以原方法执行后的结果返回给this即当前函数执行上下文。


    函数绑定

    bind完整语法:let bound = func.bind(context, [arg1], [arg2], ...);

    1. for (let key in user) {
    2. if (typeof user[key] == 'function') {
    3. user[key] = user[key].bind(user);
    4. }
    5. }//如果一个对象有很多方法,并且我们都打算将它们都传递出去,那么我们可以在一个循环中完成所有方法的绑定:

    对函数的封装

    对方法的封装:cache例子

    利用hash函数来接收多个参数生成cacheMap的键值,

    1. let worker = {
    2. slow(min, max) {
    3. alert(`Called with ${min},${max}`);
    4. return min + max;
    5. }
    6. };
    7. function cachingDecorator(func, hash) {
    8. let cache = new Map();
    9. return function() {
    10. let key = hash(arguments); // (*)
    11. if (cache.has(key)) {
    12. return cache.get(key);
    13. }
    14. let result = func.call(this, ...arguments); // (**)
    15. cache.set(key, result);
    16. return result;
    17. };
    18. }
    19. function hash(args) {
    20. return args[0] + ',' + args[1];//这种方法只能接受两个参数
    21. }
    22. worker.slow = cachingDecorator(worker.slow, hash);//
    23. alert( worker.slow(3, 5) ); // works
    24. alert( "Again " + worker.slow(3, 5) ); // same (cached)

    在hash函数中借用数组的方法arr.join()接受任意数量参数

    普通的封装,在包装器中无法访问原函数的函数属性


    防抖节流简化版,实际生产请用lodash或其他库

    防抖

    1. function debounce(func, ms) {
    2. let timeout;
    3. return function() {
    4. clearTimeout(timeout);//注意:timeout变量的生命周期,当debounce函数已经结束了后timeout仍然是有值的,不会被销毁的。所以当我们在冷却期再次调用debounce的时候这个存在内存中的timeout可以被访问。
    5. timeout = setTimeout(() => func.apply(this, arguments), ms);
    6. //注意这里this的指向问题,指向debounce被调用时的执行上下文,所以我们调用封装了func的debounce,用call/apply向func传入其执行上下文,最终执行上下文在此处跟调用了func无异。
    7. };
    8. }

    调用 debounce 会返回一个包装器。当它被调用时,它会安排一个在给定的 ms 之后对原始函数的调用,并取消之前的此类超时。
    debounce 会在“冷却(cooldown)”期后运行函数一次。适用于处理最终结果。


    节流

    1. function throttle(func, ms) {
    2. let isThrottled = false,
    3. savedArgs,
    4. savedThis;
    5. function wrapper() {
    6. if (isThrottled) { // (2)
    7. savedArgs = arguments;
    8. savedThis = this;
    9. return;
    10. }
    11. func.apply(this, arguments); // (1)
    12. isThrottled = true;
    13. setTimeout(function() {
    14. isThrottled = false; // (3)
    15. if (savedArgs) {
    16. wrapper.apply(savedThis, savedArgs);
    17. savedArgs = savedThis = null;
    18. }
    19. }, ms);
    20. }
    21. return wrapper;
    22. }
    23. let i = 1;
    24. let f = throttle(() => console.log(i++),1000);
    25. f();
    26. f();
    27. f();
    28. //最终打印结果:
    29. 1
    30. 2

    调用 throttle(func, ms) 返回 wrapper。
    1.在第一次调用期间,wrapper 只运行 func 并设置冷却状态(isThrottled = true)。
    2.在这种状态下,所有调用都记忆在 savedArgs/savedThis 中。请注意,上下文和参数(arguments)同等重要,应该被记下来。我们同时需要他们以重现调用。
    3.……然后经过 ms 毫秒后,触发 setTimeout。冷却状态被移除(isThrottled = false),如果我们忽略了调用,则将使用最后记忆的参数和上下文执行 wrapper。
    第 3 步运行的不是 func,而是 wrapper,因为我们不仅需要执行 func,还需要再次进入冷却状态并设置 timeout 以重置它。


    手写call,apply,bind

    1. function person(...args) {
    2. console.log(this.name, ...args);
    3. }
    4. let obj = {
    5. name: "jsongao",
    6. };
    7. // 手写call,参数期待一个列表,可迭代对象
    8. Function.prototype.newCall = function (obj, ...args) {
    9. obj = obj ? Object(obj) : window;
    10. let func = Symbol(1);//防止对象内本身有同名属性造成覆盖
    11. obj.[func] = this; //person调用挂载在原型上的newCall,newCall中的this本指向person。但在内部this赋值给了obj.func,person和obj.func指向同一内存地址。
    12. obj.[func](...args); //相当于在obj内部调用person,person中的this就指向obj
    13. delete obj.func;
    14. };
    15. person.newCall(obj, 1, 2, "jsongao"); //jsongao 1 2 jsongao
    16. // 手写apply,参数期待一个类组数对象
    17. Function.prototype.newApply = function (obj, args = []) {
    18. obj = obj ? Object(obj) : window;
    19. if (args && !(args instanceof Array)) {
    20. throw "参数不是类数组对象";
    21. }
    22. let func = Symbol(1);
    23. obj.[func] = this;
    24. obj.[func](args);
    25. delete obj.func;
    26. };
    27. person.newApply(obj, [1, 2, "jsongao"]); //jsongao[(1, 2, "jsongao")];
    28. person.newApply(obj); //jsongao []
    29. person.newApply(obj, 1, 2, "jsongao"); // throw "参数不是类数组对象";
    30. //手写bind,返回一个函数存储在变量中
    31. Function.prototype.newBind = function (obj, ...argsDefine) {
    32. return (...argsCall) => {
    33. let func = Symbol(1);
    34. obj.[func] = this;
    35. obj.[func](...argsDefine, ...argsCall); //(...argsDefine.concat(argsCall))
    36. delete obj.func;
    37. };
    38. };
    39. let bind = person.newBind(obj, "jsongao1", "jsongao2", "jsongao3");
    40. bind("jsongao4");

    递归和循环,遍历/ 数据结构:数组、链表

    1. let list = {
    2. value: 1,
    3. next: {
    4. value: 2,
    5. next: {
    6. value: 3,
    7. next: {
    8. value: 4,
    9. next: null,
    10. },
    11. },
    12. },
    13. };
    14. /* 循环反向输出单链表 */
    15. let a = (function reversePrintList(list) {
    16. let tmp = list;
    17. let arr = [];
    18. while (tmp) {
    19. arr.push(tmp.value);
    20. tmp = tmp.next;
    21. }
    22. for (i = arr.length - 1; i >= 0; i--) {
    23. console.log(arr[i]);
    24. }
    25. })(list); //4 3 2 1
    26. /* 递归反向输出单链表 */
    27. (function reversePrintList(list) {
    28. if (list.next) {
    29. reversePrintList(list.next);
    30. }
    31. console.log(list.value);
    32. })(list); //4 3 2 1

    承接递归,看另一个例子(任意数量的括号求和)

    1. 写一个函数 sum,它有这样的功能:
    2. sum(1)(2) == 3; // 1 + 2
    3. sum(1)(2)(3) == 6; // 1 + 2 + 3
    4. sum(5)(-1)(2) == 6
    5. sum(6)(-1)(-2)(-3) == 0
    6. sum(0)(1)(2)(3)(4)(5) == 15

    1.为了使整个程序无论如何都能正常工作,sum 的结果必须是函数。
    2.这个函数必须将两次调用之间的当前值保存在内存中。
    3.根据这个题目,当函数被用于 == 比较时必须转换成数字。函数是对象,所以转换规则会按照 对象 — 原始值转换 章节所讲的进行,我们可以提供自己的方法来返回数字。

    1. function sum(a) {
    2. let currentSum = a;
    3. function f(b) {
    4. currentSum += b;
    5. return f;//funtion f返回自己本身,但并无调用
    6. }//在这里只是声明function f,但并调用
    7. f.toString = function() {
    8. return currentSum;
    9. };
    10. return f;//sum返回function f本身,f属于闭包
    11. }
    12. alert( sum(1)(2) ); // 3
    13. alert( sum(5)(-1)(2) ); // 6
    14. alert( sum(6)(-1)(-2)(-3) ); // 0
    15. alert( sum(0)(1)(2)(3)(4)(5) ); // 15

    请注意 sum 函数只工作一次,它返回了函数 f。
    然后,接下来的每一次子调用,f 都会把自己的参数加到求和 currentSum 上,然后 f 自身。
    在 f 的最后一行没有递归。
    递归是这样子的:

    1. function f(b) {
    2. currentSum += b;
    3. return f(); // <-- 递归调用
    4. }

    这个 f 会被用于下一次调用,然后再次返回自己,按照需要重复。然后,当它被用做数字或字符串时 —— toString 返回 currentSum。我们也可以使用 Symbol.toPrimitive 或者 valueOf 来实现转换。

    添加新批注
    在作者公开此批注前,只有你和作者可见。
    回复批注