最近在看一个github大神写的博客,看完之后有醍醐灌顶的感觉,前端迭代太快,新的框架层出不穷,我觉得学习不应该只是懂得一堆API该如何使用,更应该去了解一些底层原理,有句话我觉得说的很对,框架能够让我们跑的更快,但只有了解原生的JS才能让我们走的更远。最近准备放下react或者vue等知识的学习,静下心来沉淀这些底层的东西,因为学这个的感觉比学习API让人激动多了。
声明
- 来源:mqyqingfeng blog
- 个人学习喜欢掌握几个keywords,因此博客做点小总结,学习请前往mqyqingfeng blog
原型和原型链
- 来源:原型和原型链
- prototype
- prototype是函数才会有的属性
- 函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1和 person2的原型。
- __proto__
- 这是每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。
console.log(person.__proto__ === Person.prototype); // true
- 来源于Object.prototype
- 这是每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。
- constructor
- 每个原型都有一个 constructor 属性指向关联的构造函数
console.log(Person === Person.prototype.constructor); // true
- 实际开发中可能看到person.constructor的用法,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性
- 每个原型都有一个 constructor 属性指向关联的构造函数
继承
《你不知道的JavaScript》:继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。
实例与原型
- 当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
- 原型的原型
- 实例原型也是一个对象,也具有__proto__属性指向构造函数的prototype
- 原型链
- 顶层对象Object的prototype的实例呢?
console.log(Object.prototype.__proto__ === null) // true
- 这张图片太经典,忍不住想保存
- 顶层对象Object的prototype的实例呢?
词法作用域
- 来源:词法作用域
- JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
- 词法作用域:函数的作用域在函数定义的时候就决定了。
- 动态作用域:函数的作用域是在函数调用的时候才决定的。
《JavaScript权威指南》
JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数f()定义在这个作用域链里,其中的变量 scope一定是局部变量,不管何时何地执行函数f(),这种绑定在执行f()时依然有效。
理解这个是理解闭包的基础。
执行上下文栈
- 来源:执行上下文栈
- 变量提升:变量在声明的时候,会将所有变量在作用域头部进行前置声明,但是赋值保留在原位置。
- 函数提升:函数声明时,会自动将函数前置声明,这也是为什么函数后声明,但是前面可以调用的原因,也不存在类似C语言的手动前置声明。
- 思考,如果同时存在函数声明和变量声明,并且名称一样,会怎么处理呢?
- 因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
- JavaScript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个
准备工作
,比如函数提升和变量提升。那么具体遇到什么代码会进行准备工作呢?全局代码
、函数代码
、eval代码
。 - 执行上下文栈: JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
- 工作原理:底部永远有全局执行上下文(globalContext),当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
执行上下文
当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。对于每个执行上下文,都有三个重要属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
变量对象
来源:变量对象
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
- 全局上下文
- 在顶层JavaScript代码中,可以用关键字this引用全局对象。
- 在顶层JavaScript代码中声明的所有变量都将成为全局对象的属性。
函数上下文
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。
活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫activation object,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
执行上下文的代码处理分成两个阶段进行处理:分析和执行
- 分析:活动对象AO的组成有:arguments (初始化阶段)+ 所有形参(分析阶段) + 函数声明(分析阶段) + 变量声明(分析阶段)
- 执行:在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值
作用域链
来源:作用域链
为什么函数的作用域在函数定义的时候就决定了呢?从作用域链的角度可以更加理解。创建和激活两个时期来讲解作用域链是如何创建和变化的
- 函数创建
- 函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
- 函数激活
- 当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。
Scope = [AO].concat([[Scope]]);
- 当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。
- 从checkscope函数执行上下文中作用域链和变量对象的创建过程
- 函数被创建,保存作用域链到 内部属性[[scope]]
- 开始执行函数,创建函数执行上下文,函数执行上下文被压入执行上下文栈
- 准备工作
- 第一步:复制函数[[scope]]属性创建作用域链
- 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
- 第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]] }
- 执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: 'local scope' }, Scope: [AO, [[Scope]]] }
- 函数执行完毕,函数上下文从执行上下文栈中弹出
this
来源:this
看第一遍,智商不够,完全看不懂,选择不说话。第二遍,懂一丢丢,总结两句。
- Types
- ECMAScript 的类型分为语言类型和规范类型。
- ECMAScript 语言类型是开发者直接使用 ECMAScript 可以操作的。其实就是我们常说的Undefined, Null, Boolean, String, Number, 和 Object。
- ECMAScript 规范中还有一种只存在于规范中的类型,它们的作用是用来描述语言底层行为逻辑。
Reference
这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。
- 组成
- base:属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种。
- name:属性的名称
- strict:严格默认?不懂
- 方法:
- GetBase:得到Reference的base值
- IsPropertyReference:如果 base value是一个对象,就返回true。
- GetValue: 从 Reference 类型获取对应值的方法,调用 GetValue,返回的将是具体的值,而不再是一个 Reference,这个很重要!
- ImplicitThisValue:该函数始终返回 undefined。
- 组成
- 那么this和Reference的关系?如何确定this值呢?
- 计算
MemberExpression
的结果赋值给 ref- 简单理解MemberExpression 其实就是()左边的部分。
- 判断 ref 是不是一个 Reference 类型
- 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
- 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
- 如果 ref 不是 Reference,那么 this 的值为 undefined,非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象。
- 规则列出来了,那么判断 ref 是不是一个 Reference 类型。关键就在于看规范是如何处理各种 MemberExpression,返回的结果是不是一个Reference类型。
- 属性访问方式(foo.bar):ref是一个Reference类型。
- 分组操作符((express)):最终ref的类型根据组内表达式决定。如果依旧是属性访问方式,则依旧是Reference类型,更多情况下面详细说明。
- 组内是赋值操作:会调用GetValue函数,返回的将是具体的值,不再是Reference
- 组内逻辑与算法:会调用GetValue函数,返回的将是具体的值,不再是Reference
- 组内逗号操作符:会调用GetValue函数,返回的将是具体的值,不再是Reference
- 最简单情形:foo(),此时Reference 类型如下,看了类型就知道怎么分析了,属于第2种情况:
var fooReference = { base: EnvironmentRecord, name: 'foo', strict: false };
- 计算
执行上下文的具体处理过程
- 执行全局代码,创建全局执行上下文并压入执行上下文栈。
- 全局上下文初始化,三部分:VO,Scope(作用域链),this
- 某函数A被创建,保存作用域链到函数的内部属性[[scope]]
- 执行A函数,创建函数执行上下文并压入执行上下文栈
- 函数执行上下文初始化,四步骤
- 复制函数 [[scope]] 属性创建作用域链,三部分:AO,Scope(作用域链),this
- 用 arguments 创建活动对象
- 初始化活动对象,即加入形参、函数声明、变量声明
- 将活动对象压入 checkscope 作用域链顶端
- 如果函数内部又创建了函数,则重复3-5步骤
- 函数执行完毕,函数上下文从执行上下文栈中弹出