JavaScript 实践+理论(总结篇):作用域、闭包、this、对象原型
作用域与闭包
第一章 作用域是什么
- • 作用域:根据标识符查找变量的一套规则。
- • 嵌套作用域:从当前作用域开始查找变量,如果找不到就向上一层继续查找,直到找到最外层的全局作用域为止。
- • 严格模式与非严格模式下引擎查找规则:
- • 严格模式:
- • 非严格模式:
- 1. 引擎执行 RHS 时若找不到该标识符,会抛出
ReferenceError
- 2. 引擎执行 LHS 时若找不到该标识符,会隐式地在全局作用域中创建一个该名称的变量,并将其返回给引擎。
- 1. 在
use strict
模式下禁止自动或隐式地创建全局变量,所以在引擎执行 LHS 时,不会再隐式地创建一个全局变量,而是直接抛出一个ReferenceError
。 - 2. 在该模式下,RHS 找到一个变量当对这个变量进行不合规的操作时会抛出一个
TypeError
, 而ReferenceError
代表着在作用域查找或判断失败,TypeError
代表作用域查找成功了,但对该变量的操作不合规。
- • 引擎的查找规则:
- 1. LHS: 赋值操作的目标
- 2. RHS: 赋值操作的源头
第二章 词法作用域
- • 作用域查找规则:从当前所处作用域最内部开始,逐级向上查找,直到找到第一个匹配的标识符为止。并且词法作用域只会查找一级标识符,如果 foo.bar.baz,词法作用域只会试图查找 foo 标识符,然后再分别访问 bar 和 baz。
- • 函数不论是在哪里被调用,或如何被调用,它的词法作用域都是由被声明时所处的位置决定。
- • 非严格模式下, eval(...) 中的语句会修改 eval(...) 所处的词法作用域。
- • 严格模式下, eval(...) 在运行时有自己词法作用域,不会修改所处作用域。
- • with(...) 会将当前对象的引用当做作用域来处理,将对象中的属性当做作用域中的标识符来处理,从而创建一个新的词法作用域。
附录 A 动态作用域
- • 作用域是基于调用栈的,而不是代码中的作用域嵌套的。
- • 动态作用域是在运行时确定的
- • 词法作用域关注函数从何处声明
- • 动态作用域关注函数从何处调用
第三章 函数作用域和块作用域
- • 如何区分函数声明和函数表达式:如果 function 为声明中的第一个关键字,那它就是一个函数声明,否则就是一个函数表达式。
- • IIFE(立即执行函数表达式),第一个() 将函数变成表达式,第二个() 将执行这个函数。且第二个 () 可放在第一个 () 内最后位置,且含义相同。
- • 在 IIFE 中可在第二个 () 中传递参数,在第一个 () 中的形参就是第二个 () 所传进去的参数。
- • var 声明符写在哪里都是一样的,因为它会变量提升。
- • let 声明符声明的变量和函数不会被提升,何为提升,就是在代码执行时是否有被声明过,如果没有声明过则直接抛出错误。
第四章 提升
- 1. 先有鸡(声明),再有蛋(赋值)
- 2. 如
var a = 2;
这段声明代码 JavaScript 引擎会将他们分为var a
和a = 2;
两个单独的声明来处理,第一个是在编译阶段所执行,第二个是在执行阶段所执行。 - 3. 重复定义的函数声明,后面的会覆盖前面的。
- 4. 函数声明会被提升,而函数表达式不会被提升
- 5. 只有函数本身会被提升, 而函数表达式在内的赋值操作并不会被提升。
第五章 作用域闭包
- 1. 何为闭包:当函数可以记住并访问所在的词法作用域时,即使函数在当前词法作用域之外执行,这时就会产生闭包。
- 2. 严格意义上来说,一个函数返回另一个函数。
3. 空的 IIFE 并不是闭包,虽然通过 IIFE 改造有用了更多的词法作用域,但在 IIFE 中的所创建的作用域是封闭起来的。只能通过从外传入一个参数到 IIFE 中被使用时,才是闭包。
for(var = 1 ; i <= 5; i++){ (function() { var j = i; setTimeout(function timer() { console.log(j); }, j * 1000); })(); } // 再次改进后 for(var = 1 ; i <= 5; i++){ (function(j) { setTimeout(function timer() { console.log(j); }, j * 1000); })(i); }
this 与对象原型
第一章 关于 this
- 1. this 既不指向函数自身也不指向函数的词法作用域
- 2. this 是在函数被调用时发生的绑定关系,它指向哪里完全取决于函数在哪里被调用
第二章 this 全面解析
- • 判断 this 指向的四种规则:
- 1. 是否在 new 中调用(new 调用), this 指向新创建的对象
function Foo() { // do something } let f = new Foo(); // call() function foo() { console.log(this.a); } var obj = { a: 2, }; foo.call(obj); // 2 // apply() function foo(something) { console.log(this.a, something); return this.a + something; } var obj = { a: 2, }; var bar = function () { return foo.apply(obj, arguments); }; var b = bar(3); // 2 3 console.log(b); // 5 // bind() function foo(something) { this.a = something; } var obj1 = {}; var bar = foo.bind(obj1); bar(2); console.log(obj1.a); // 2 var baz = new bar(3); console.log(obj1.a); // 2 console.log(baz.a); // 3 // eg1: function foo() { console.log(this.a); // 2 } var obj = { a: 2, foo: foo, }; obj.foo(); // eg2: function foo() { console.log(this.a); } var obj2 = { a: 42, foo: foo, }; var obj1 = { a: 2, obj2: obj2, }; obj1.obj2.foo(); // 42 function foo() { console.log(this.a); } var a = 2; foo(); // 2 // 严格模式下的位置 function foo() { 'use strict'; console.log(this.a); } var a = 2; foo(); // Type: this is undefined function foo() { console.log(this.a); } var a = 2; (function () { 'use strict'; foo(); // 2 });
- 1. 如果都不是,则是默认绑定,在严格模式下,this 指向 undefined。非严格模式下, this 指向全局对象。
- 1. 是否在某个对象中调用(隐式绑定), this 指向绑定对象的上下文
- 1. 是否通过 call, apply(显示绑定), this 指向绑定的对象
- 2. 箭头函数不会使用上述四条规则,而是根据当前的词法作用域来决定 this 的,箭头函数会继承外层函数的 this。
- 3. 注意:对于默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined, 否则 this 会绑定到全局对象。
- 4. 优先级问题
- • 显式绑定:call()、apply()。(硬绑定也是显式绑定的其中一种: bind())
- • new 绑定: new Foo()
- • 隐式绑定: obj.foo();
- • 默认绑定: foo();
- •
排序:显式绑定 > new 绑定 > 隐式绑定 > 默认绑定