递归和堆栈

Rest 参数与 Spread 语法

当我们在代码中看到 “…” 时,它要么是 rest 参数,要么就是 spread 语法。
有一个简单的方法可以区分它们:

  • 若 … 出现在函数参数列表的最后,那么它就是 rest 参数,它会把参数列表中剩余的参数收集到一个数组中。
  • 若 … 出现在函数调用或类似的表达式中,那它就是 spread 语法,它会把一个数组展开为列表。

使用场景:

  • Rest 参数用于创建可接受任意数量参数的函数。
  • Spread 语法用于将数组传递给通常需要含有许多参数的列表的函数。

它们俩的出现帮助我们轻松地在列表和参数数组之间来回转换。

变量作用域,闭包! https://zh.javascript.info/closure

如果在函数被创建之后,外部变量发生了变化会怎样?函数会获得新值还是旧值?
在 JavaScript 中,有三种声明变量的方式:let,const(现代方式),var(过去留下来的方式)。

  • 在本文的示例中,我们将使用 let 声明变量。
  • 用 const 声明的变量的行为也相同(译注:与 let 在作用域等特性上是相同的),因此,本文也涉及用 const 进行变量声明。
  • 旧的 var 与上面两个有着明显的区别,我们将在 旧时的 “var” 中详细介绍。

    代码块

    嵌套函数

    词法环境

    Step 1. 变量

    在 JavaScript 中,每个运行的函数,代码块 {…} 以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。
    右侧的矩形演示了执行过程中全局词法环境的变化:
  1. 当脚本开始运行,词法环境预先填充了所有声明的变量
    • 最初,它们处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是在用 let 声明前,不能引用它。几乎就像变量不存在一样。
  2. 然后 let phrase 定义出现了。它尚未被赋值,因此它的值为 undefined。从这一刻起,我们就可以使用变量了。
  3. phrase 被赋予了一个值。
  4. phrase 的值被修改。

    Step 2. 函数声明

    一个函数其实也是一个值,就像变量一样。
    不同之处在于函数声明的初始化会被立即完成。
    当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像 let 那样直到声明处才可用)。
    这就是为什么我们可以在(函数声明)的定义之前调用函数声明。

    Step 3. 内部和外部的词法环境

    当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
    如果在任何地方都找不到这个变量,那么在严格模式下就会报错(在非严格模式下,为了向下兼容,给未定义的变量赋值会创建一个全局变量)。

    Step 4. 返回函数

    在变量所在的词法环境中更新变量。

    垃圾收集

    通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除。因为现在没有任何对它们的引用了。与 JavaScript 中的任何其他对象一样,词法环境仅在可达时才会被保留在内存中。

    实际开发中的优化

    理论上当函数可达时,它外部的所有变量也都将存在。
    但在实际中,JavaScript 引擎会试图优化它。它们会分析变量的使用情况,如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。
    在 V8(Chrome,Edge,Opera)中的一个重要的副作用是,此类变量在调试中将不可用。

闭包(?!不太懂。。。

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

文末题目 作用域、闭包

题目1:函数会选择最新的内容吗?

函数 sayHi 使用外部变量。当函数运行时,将使用哪个值?

  1. let name = "John";
  2. function sayHi() {
  3. alert("Hi, " + name);
  4. }
  5. name = "Pete";
  6. sayHi(); // 会显示什么:"John" 还是 "Pete"?

答案:Pete
函数将从内到外依次在对应的词法环境中寻找目标变量它使用最新的值
旧变量值不会保存在任何地方。当一个函数想要一个变量时,它会从自己的词法环境或外部词法环境中获取当前值。

题目2:Counter 对象

  1. function Counter() {
  2. let count = 0;
  3. this.up = function() {
  4. return ++count;
  5. };
  6. this.down = function() {
  7. return --count;
  8. };
  9. }
  10. let counter = new Counter();
  11. alert( counter.up() ); // ?
  12. alert( counter.up() ); // ?
  13. alert( counter.down() ); // ?

这两个嵌套函数都是在同一个词法环境中创建的,所以它们可以共享对同一个 count 变量的访问

  1. alert( counter.up() ); // 1
  2. alert( counter.up() ); // 2
  3. alert( counter.down() ); // 1

题目3:闭包 sum

编写一个像 sum(a)(b) = a+b 这样工作的 sum 函数。sum(1)(2)=3
为了使第二个括号有效,第一个(括号)必须返回一个函数。

  1. function sum(a) {
  2. return function(b) {
  3. return a + b; // 从外部词法环境获得 "a"
  4. };
  5. }
  6. alert( sum(1)(2) ); // 3
  7. alert( sum(5)(-1) ); // 4

题目4:变量可见吗?

下面这段代码的结果会是什么?

  1. let x = 1;
  2. function func() {
  3. console.log(x); // ?
  4. let x = 2;
  5. }
  6. func();

答案:error
从程序执行进入代码块(或函数)的那一刻起,变量就开始进入“未初始化”状态。它一直保持未初始化状态,直至程序执行到相应的 let 语句。
换句话说,一个变量从技术的角度来讲是存在的,但是在 let 之前还不能使用。

  1. function func() {
  2. // 引擎从函数开始就知道局部变量 x,
  3. // 但是变量 x 一直处于“未初始化”(无法使用)的状态,直到结束 let(“死区”)
  4. // 因此答案是 error
  5. console.log(x); // ReferenceError: Cannot access 'x' before initialization
  6. let x = 2;
  7. }

调度:setTimeout 和 setInterval

有时我们并不想立即执行一个函数,而是等待特定一段时间之后再执行。这就是所谓的“计划调用(scheduling a call)”。
目前有两种方式可以实现:

  • setTimeout 允许我们将函数推迟到一段时间间隔之后再执行。
  • setInterval 允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。

    setTimeout

    语法

    1. let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
    参数说明:
    func|code
    想要执行的函数或代码字符串。 一般传入的都是函数。由于某些历史原因,支持传入代码字符串,但是不建议这样做。
    delay
    执行前的延时,以毫秒为单位(1000 毫秒 = 1 秒),默认值是 0;
    arg1,arg2…
    要传入被执行函数(或代码字符串)的参数列表(IE9 以下不支持)

    示例

    sayHi() 方法会在 1 秒后执行: ```javascript function sayHi() { alert(‘Hello’); }

setTimeout(sayHi, 1000);

  1. 带参数的情况:
  2. ```javascript
  3. function sayHi(phrase, who) {
  4. alert( phrase + ', ' + who );
  5. }
  6. setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John

新手开发者有时候会误将一对括号 () 加在函数后面:

// 错的!
setTimeout(sayHi(), 1000);
//sayHi() 的执行结果是 undefined(也就是说函数没有返回任何结果)

因为 setTimeout 期望得到一个对函数的引用。而这里的 sayHi() 很明显是在执行函数,所以实际上传入 setTimeout 的是 函数的执行结果

用 clearTimeout 来取消调度

let timerId = setTimeout(...);
clearTimeout(timerId);

setInterval

setInterval 方法和 setTimeout 的语法相同。所有参数的意义也是相同的。不过与 setTimeout 只执行一次不同,setInterval 是每间隔给定的时间周期性执行。
想要阻止后续调用,我们需要调用 clearInterval(timerId)

// 每 2 秒重复一次
let timerId = setInterval(() => alert('tick'), 2000);

// 5 秒之后停止
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);

嵌套的 setTimeout

周期性调度有两种方式。
一种是使用 setInterval,另外一种就是嵌套的 setTimeout,就像这样:

/** instead of:
let timerId = setInterval(() => alert('tick'), 2000);
*/

let timerId = setTimeout(function tick() {
  alert('tick');
  timerId = setTimeout(tick, 2000); // (*)
}, 2000);

嵌套的 setTimeout 能够精确地设置两次执行之间的延时,而 setInterval 却不能。

使用 setInterval 时,func 函数的实际调用间隔要比代码中设定的时间间隔要短!
这也是正常的,因为 func 的执行所花费的时间“消耗”了一部分间隔时间。
也可能出现这种情况,就是 func 的执行所花费的时间比我们预期的时间更长,并且超出了 100 毫秒。
在这种情况下,JavaScript 引擎会等待 func 执行完成,然后检查调度程序,如果时间到了,则 立即 再次执行它。
极端情况下,如果函数每次执行时间都超过 delay 设置的时间,那么每次调用之间将完全没有停顿。

嵌套的 setTimeout 就能确保延时的固定(这里是 100 毫秒)。
这是因为下一次调用是在前一次调用完成时再调度的。

零延时的 setTimeout

这儿有一种特殊的用法:setTimeout(func, 0),或者仅仅是 setTimeout(func)。
这样调度可以让 func 尽快执行。但是只有在当前正在执行的脚本执行完成后,调度程序才会调用它。
也就是说,该函数被调度在当前脚本执行完成“之后”立即执行
下面这段代码会先输出 “Hello”,然后立即输出 “World”:

setTimeout(() => alert("World"));

alert("Hello");

装饰器模式和转发,call/apply

JavaScript 在处理函数时提供了非凡的灵活性。它们可以被传递,用作对象,现在我们将看到如何在它们之间 转发(forward) 调用并 装饰(decorate) 它们。
?这个知识点很陌生。。先跳过。。

函数绑定

丢失 “this”

解决方案 1:包装器

解决方案 2:bind

方法 func.bind(context, …args) 返回函数 func 的“绑定的(bound)变体”,它绑定了上下文 this 和第一个参数(如果给定了)。
通常我们应用 bind 来绑定对象方法的 this,这样我们就可以把它们传递到其他地方使用。例如,传递给 setTimeout。
举个例子,这里的 funcUser 将调用传递给了 func 同时 this=use

let user = {
  firstName: "John"
};

function func() {
  alert(this.firstName);
}

let funcUser = func.bind(user);
funcUser(); // John

这里的 func.bind(user) 作为 func 的“绑定的(bound)变体”,绑定了 this=user。

let user = {
  firstName: "John"
};

function func(phrase) {
  alert(phrase + ', ' + this.firstName);
}

// 将 this 绑定到 user
let funcUser = func.bind(user);

funcUser("Hello"); // Hello, John(参数 "Hello" 被传递,并且 this=user)

深入理解箭头函数

箭头函数不仅仅是编写简洁代码的“捷径”。它还具有非常特殊且有用的特性。

箭头函数没有 “this”

箭头函数没有 this。如果访问 this,则会从外部获取。
例如,我们可以使用它在对象方法内部进行迭代:

let group = {
  title: "Our Group",
  students: ["John", "Pete", "Alice"],

  showList() {
    this.students.forEach(
      student => alert(this.title + ': ' + student) 
      // this.title 其实和外部方法 showList 的完全一样。那就是:group.title
    );
  }
};

group.showList();

这里 forEach 中使用了箭头函数,所以其中的 this.title 其实和外部方法 showList 的完全一样。那就是:group.title。

箭头函数 VS bind

箭头函数 => 和使用 .bind(this) 调用的常规函数之间有细微的差别:

  • .bind(this) 创建了一个该函数的“绑定版本”。
  • 箭头函数 => 没有创建任何绑定。箭头函数只是没有 this。this 的查找与常规变量的搜索方式完全相同:在外部词法环境中查找

    箭头函数没有 “arguments”

    总结

    箭头函数:

  • 没有 this

  • 没有 arguments
  • 不能使用 new 进行调用
  • 它们也没有 super,但目前我们还没有学到它。我们将在 类继承 一章中学习它。

    属性标志和属性描述符

    属性的 getter 和 setter