JavaScript 实际 + 实践(总结篇):作用域、闭包、this、对象原型
系列首发于公众号『前端进阶圈』,若不想错过更多精彩内容,请“星标”一下,敬请关注公众号最新消息。
作用域与闭包
第一章 作用域是什么
- 作用域:依据标识符查找变量的一套规定。
- 嵌套作用域:从以后作用域开始查找变量,如果找不到就向上一层持续查找,直到找到最外层的全局作用域为止。
-
严格模式与非严格模式下引擎查找规定:
-
严格模式:
- 在
use strict
模式下禁止主动或隐式地创立全局变量,所以在引擎执行 LHS 时,不会再隐式地创立一个全局变量,而是间接抛出一个ReferenceError
。 - 在该模式下,RHS 找到一个变量当对这个变量进行不合规的操作时会抛出一个
TypeError
, 而ReferenceError
代表着在作用域查找或判断失败,TypeError
代表作用域查找胜利了,但对该变量的操作不合规。
- 在
-
非严格模式:
- 引擎执行 RHS 时若找不到该标识符,会抛出
ReferenceError
- 引擎执行 LHS 时若找不到该标识符,会隐式地在全局作用域中创立一个该名称的变量,并将其返回给引擎。
- 引擎执行 RHS 时若找不到该标识符,会抛出
-
-
引擎的查找规定:
- LHS: 赋值操作的指标
- RHS: 赋值操作的源头
第二章 词法作用域
- 作用域查找规定:从以后所处作用域最外部开始,逐级向上查找,直到找到第一个匹配的标识符为止。并且词法作用域只会查找一级标识符,如果 foo.bar.baz,词法作用域只会试图查找 foo 标识符,而后再别离拜访 bar 和 baz。
- 函数不论是在哪里被调用,或如何被调用,它的词法作用域都是由被申明时所处的地位决定。
- 非严格模式下, eval(…) 中的语句会批改 eval(…) 所处的词法作用域。
- 严格模式下, eval(…) 在运行时有本人词法作用域,不会批改所处作用域。
- with(…) 会将以后对象的援用当做作用域来解决,将对象中的属性当做作用域中的标识符来解决,从而创立一个新的词法作用域。
附录 A 动静作用域
- 作用域是基于调用栈的,而不是代码中的作用域嵌套的。
- 动静作用域是在运行时确定的
- 词法作用域关注函数从何处申明
- 动静作用域关注函数从何处调用
第三章 函数作用域和块作用域
- 如何辨别函数申明和函数表达式:如果 function 为申明中的第一个关键字,那它就是一个函数申明,否则就是一个函数表达式。
- IIFE(立刻执行函数表达式),第一个() 将函数变成表达式,第二个() 将执行这个函数。且第二个 () 可放在第一个 () 内最初地位,且含意雷同。
- 在 IIFE 中可在第二个 () 中传递参数,在第一个 () 中的形参就是第二个 () 所传进去的参数。
- var 申明符写在哪里都是一样的,因为它会变量晋升。
- let 申明符申明的变量和函数不会被晋升,何为晋升,就是在代码执行时是否有被申明过,如果没有申明过则间接抛出谬误。
第四章 晋升
- 先有鸡(申明), 再有蛋(赋值)
- 如
var a = 2;
这段申明代码 JavaScript 引擎会将他们分为var a
和a = 2;
两个独自的申明来解决,第一个是在编译阶段所执行,第二个是在执行阶段所执行。 - 反复定义的函数申明,前面的会笼罩后面的。
- 函数申明会被晋升,而函数表达式不会被晋升
- 只有函数自身会被晋升,而函数表达式在内的赋值操作并不会被晋升。
第五章 作用域闭包
- 何为闭包:当函数能够记住并拜访所在的词法作用域时,即便函数在以后词法作用域之外执行,这时就会产生闭包。
- 严格意义上来说,一个函数返回另一个函数。
- 空的 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
- this 既不指向函数本身也不指向函数的词法作用域
- this 是在函数被调用时产生的绑定关系,它指向哪里齐全取决于函数在哪里被调用
第二章 this 全面解析
- 判断 this 指向的四种规定:
-
是否在 new 中调用(new 调用), this 指向新创建的对象
function Foo() {// do something} let f = new Foo();
- 是否通过 call, apply(显示绑定), this 指向绑定的对象
// 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
- 是否在某个对象中调用(隐式绑定), this 指向绑定对象的上下文
// 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
- 如果都不是,则是默认绑定,在严格模式下,this 指向 undefined。非严格模式下, this 指向全局对象。
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});
- 箭头函数不会应用上述四条规定,而是依据以后的词法作用域来决定 this 的,箭头函数会继承外层函数的 this。
- 留神:对于默认绑定来说,决定 this 绑定对象的并不是调用地位是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined, 否则 this 会绑定到全局对象。
-
优先级问题
- 显式绑定:call()、apply()。(硬绑定也是显式绑定的其中一种: bind())
- new 绑定: new Foo()
- 隐式绑定: obj.foo();
- 默认绑定: foo();
排序:显式绑定 > new 绑定 > 隐式绑定 > 默认绑定
第三章 对象
- 对象一共有两种语法:文字模式 (
var obj = {....}
) 和结构模式(var obj = new Object()
)。两种模式的惟一区别在于文字申明可增加多个键值对,而结构模式必须一一增加。 -
在 JavaScript 中为什么 typeof null 会返回 object?
- 因为不同的对象在底层都是通过二进制示意的,在 JavaScript 中二进制前三位都是 0 的话会被判断为 object 类型,而 null 的二进制都为 0,天然前三位也是 0,所以执行 typeof 时会返回 object。
- 对象属性拜访中通过
.
操作符拜访被称为属性拜访,通过[]
操作符拜访被称为键拜访。 -
对象操作的快捷办法:
-
在已有属性的对象上禁止扩大其余属性:Object.preventExtensions()
- 严格模式: 抛出 TypeError 谬误
- 非严格模式:静默失败
- 密封一个对象,既不能重新配置和删除现有属性(即时是可批改属性): Object.seal()
- 解冻一个对象,既不能增加,删除,批改:Object.freeze()
- 判断一个属性是否在对象的可枚举属性中: xxx.propertyIsEnumerable(‘xxxx’)
-
- in 操作符会查看属性是否对象及其 [[Prototype]] 原型链中,而 hasOwnProperty(),propertyIsEnumerable() 只会查看属性是否在某个对象中,不会查看 [[Prototype]] 原型链。
- Object.keys(…) 会返回一个数组,蕴含所有可枚举属性,Object.getOwnPropertyNames(…)会返回一个数组,蕴含所有属性,无论他们是否可枚举。
第四章 混合对象的类
- 多态:父类的一些通过行为能够被子类的行为重写
- 父类与子类之间的继承其实就是复制。
- 一个类就是一个蓝图,也就只是一个打算,并不是真正能够交互的对象,必须通过实例化对象来调用所有的个性,而实例化对象就是类的所有个性的一个正本。
- 在类被继承时,行为也会被复制到子类中。
第五章 原型
- 当拜访对象中一个不存在的属性时,[[Get]] 操作就会查找对象外部的 [[Prototype]] 关联的对象,而这个关联关系就像是嵌套的作用域,在查找属性时会对其进行遍历查找。直到找到一般对象内置的 Object.prototype 顶端,如果找不到就会进行。
- 关联两个对象最罕用的办法就是用 new 关键字调用,因为在调用的第四个步骤中会关联到所创立的新对象。
- 应用 for…in 遍历对象和 in 操作符时都会查找对象的整条原型链。(无论属性是否可枚举)
var anotherObject = {a: 2,};
// 创立一个关联到 anotherObject 的对象
var myObject = Object.create(anotherObject);
for (var k in myObject) {console.log('found:' + k);
}
// found: a
'a' in myObject; // true
- 留神:
当你通过各种语法进行属性查找时都会查找 [[Prototype]] 链,直到找到属性或找到残缺的原型链。
-
但到哪是 [[Prototype]] 的止境呢?
- 所有一般的 [[Prototype]] 链最终都会指向内置的 Object.prototype。
-
如果对象中的某个属性不间接存在于某个对象上时会产生以下几种状况:
myObject.foo = 'bar';
- 如果在 [[Prototype]] 原型链下层存在 foo 拜访属性,并且没有被标记为只读(writable: false), 那就会间接在 myObject 中增加一个 foo 属性,则它是屏蔽属性。
let a = {foo: 'atxt',}; let c = Object.create(a); c.foo = 'cfoo'; console.log('c ------>', c.foo); // atxt
- 如果在 [[Prototype]] 原型链上存在 foo 属性,然而被标记为只读, 那就无奈批改已有属性或在 myObject 上创立屏蔽属性。如果在严格模式下运行,会间接抛出一个谬误。否则,这条赋值语句就会被疏忽。总之,不会产生屏蔽。
let a = {foo: 'atxt',}; Object.defineProperty(a, 'foo', {writable: false,}); let c = Object.create(a); c.foo = 'cfoo'; console.log('c ------>', c.foo); // atxt
- 如果在 [[Prototype]] 原型链下层存在 foo 并且它是一个 setter,那就肯定会调用这个 setter。foo 不会被增加到(能够说屏蔽到) myObject 中,也不会从新定义 foo 这个 setter。如下代码:
let a = {get foo() {return this._foo_;}, set foo(v) {this._foo_ = 'afoo';}, }; a.foo = 'afoo'; let c = Object.create(a); c.foo = 'cfoo'; console.log('c ------>', c.foo); // afoo // 把赋值[[put]] 操作存储到了另一个变量 _a_ 中,名称 _a_ 只是一种常规,没有任何非凡行为,与其余一般属性一样。
- 在面向类的语言中,类能够实例化屡次。
-
应用 new 调用是构造函数还是调用?
- 实际上,Foo 和一般函数没有任何区别。函数自身并不是构造函数。然而当你在一般的函数调用前加上 new 关键字后,就会把以后函数变成一个结构函数调用。实际上,new 会劫持所有一般函数并用结构对象的模式来调用它。
- 如下代码:
function NothingSpecial() {console.log("Don't mind me!"); } var a = new NothingSpecial(); // "Don't mind me!" a; // {}
- 在 JavaScript 中对于构造函数最精确的解释是,所有带 new 的函数调用。
-
何为原型链?
- [[Prototype]] 的作用: 如果在对象上没有找到须要的属性或办法援用,引擎就会技术在 [[Prototype]] 关联的对象进行查找。如果后者也没有找到须要的援用就会持续查找它的 [[Prototype]]。以此类推,这一系列的对象链接被称为 “ 原型链 ”。
- 对象之间是通过 [[Prototype]] 链关联的。
第六章 行为委托
- 行为委托认为对象之间是兄弟关系,而不是父类与子类的关系,两者互相委托。而 JavaScript 中的 [[Prototype]] 机制实质上就是委托机制。
详解篇(按程序浏览)
- JavaScript 作用域深度分析:从部分到全局一网打尽
- JavaScript 中 eval 和 with 语句如何影响作用域链:摸索深度常识
- JavaScript 作用域深度分析:动静作用域
- 【深度分析】JavaScript 中块级作用域与函数作用域
- JavaScript 深度分析之变量、函数晋升:从外表到实质
- this 之谜揭底:从浅入深了解 JavaScript 中的 this 关键字(一)
- this 之谜揭底:从浅入深了解 JavaScript 中的 this 关键字(二)
- 实践 + 实际:从原型链到继承模式,把握 Object 的精华(一)
- 实践 + 实际:从原型链到继承模式,把握 Object 的精华(二)