主要讲述什么是原型,什么是原型链,以及分别总结执行上下文的三个重要属性:变量对象、作用域链、this,还有闭包等难以理解的知识点。
从原型到原型链
图示总结
prototype
:每一个函数都有一个prototype
属性,该属性指向了一个对象,此对象为调用该函数而创建的实例的原型
__proto__
:每一个对象(除 null)都具有一个属性:__proto__
,这个属性指向该对象的原型
constructor
:每个原型都有一个constructor
属性指向关联的构造函数
Object
:原型对象是通过 Object
构造函数生成的,最后Object.prototype.__proto__ = null
原型链:由相互关联的原型(proto)组成的链状结构就是原型链,即图中蓝色的这条线
代码总结
每个函数都有一个 prototype 属性
1 | function Person() {} |
每一个 JavaScript 对象(除了 null )都具有的一个属性,叫proto,这个属性会指向该对象的原型。
构造函数的 prototype 属性,指向了调用该构造函数而创建的实例的原型 person.proto
1 | person.__proto__ === Person.prototype; // true |
每个原型都有一个 constructor 属性指向关联的构造函数
1 | Person.prototype.constructor === Person; // true |
原型对象就是通过 Object 构造函数生成的
1 | Person.prototype.__proto__ === Object.prototype; // true |
Object.prototype.proto 的值为 null,即 Object.prototype 没有原型,终止查找
1 | Object.prototype.__proto__; // null |
词法作用域和动态作用域
javascript 采用的是词法作用域(lexical scoping)
,函数的作用域是在函数定义的时候就决定了,而不是调用的时候才决定
- 词法作用域,即静态作用域,函数的作用域在函数定义的时候就决定了
- 动态作用域,函数的作用域是在函数调用的时候才决定
1 | var scope = "global scope"; |
1 | var scope = "global scope"; |
因为 JavaScript 采用的是词法作用域,函数的作用域基于函数创建的位置。
而引用《JavaScript 权威指南》的回答就是:
JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。
原文链接:JavaScript 深入之词法作用域和动态作用域
执行上下文栈
JavaScript 的可执行代码(executable code)的类型有哪些:
- 全局代码
- 函数代码
- eval 代码
函数那么多,如何管理创建的那么多执行上下文呢?
所以 JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS
)来管理执行上下文。
当执行一个函数的时候,就会创建一个执行上下文(execution context)
,并且压入执行上下文栈(Execution context stack, ESC)
当函数执行完毕的时候,会将函数的执行上下文栈
中弹出。
其实,这里就会联想到 push pop 栈堆
(后进先出-LIFO)。
模拟执行上下文栈:
1 | // 执行上下文栈是一个数组 ECStack,整个应用程序结束的时候,才会被清空 |
运行如下代码:
1 | function fun3() { |
伪代码模拟执行:(根据 push pop 原理,后进先出)
1 | ECStack.push(<fun1> functionContext); // 压入fun1上下文,发现了 fun2 被调用 |
原文链接:JavaScript 深入之执行上下文栈
变量对象
1、全局上下文的变量对象初始化:全局对象
2、函数上下文的变量对象初始化:只包括Arguments
对象
3、进入执行上下文时:给变量对象添加形参、函数声明、变量声明等初始的属性值
4、代码执行阶段:再次修改变量对象的属性值
执行过程
执行上下文过程可分为:进入执行上下文和代码执行(分析-执行)
1 | function foo(a) { |
进入执行上下文过程:
1 | AO = { |
代码执行阶段:
1 | AO = { |
总结:未进入执行阶段之前,变量对象(VO)
中的属性都不能访问!但是进入执行阶段之后,变量对象(VO)
转变为了活动对象(AO)
,里面的属性都能被访问了,然后开始进行执行阶段的操作。它们其实都是同一个对象,只是处于执行上下文的不同生命周期。
最后,函数是“第一等公民”,记住这个,变量名称和函数名称相同的声明,优先执行函数声明。
原文链接:JavaScript 深入之变量对象
作用域
对于每个执行上下文,都有三个重要属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
下面让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。
函数创建
由上节内容可知:函数的作用域在函数定义的时候就决定了。
这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
如:
1 | function foo() { |
函数激活
当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。
这时候执行上下文的作用域链,我们命名为 Scope:
1 | Scope = [AO].concat([[Scope]]); |
至此,作用域链创建完毕。
综合分析
以下面的例子为例,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:
1 | var scope = "global scope"; |
执行过程如下:
1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
1 | checkscope.[[scope]] = [ |
2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
1 | ECStack = [checkscopeContext, globalContext]; |
3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
1 | checkscopeContext = { |
4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
1 | checkscopeContext = { |
5.第三步:将活动对象压入 checkscope 作用域链顶端
1 | checkscopeContext = { |
6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
1 | checkscopeContext = { |
7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
1 | ECStack = [globalContext]; |
原文链接:JavaScript 深入之作用域链
从 ECMAScript 规范解读 this
作者曰:在写文章之初,我就面临着这些问题,最后还是放弃从多个情形下给大家讲解 this 指向的思路,而是追根溯源的从 ECMASciript 规范讲解 this 的指向,尽管从这个角度写起来和读起来都比较吃力,但是一旦多读几遍,明白原理,绝对会给你一个全新的视角看待 this 。
1 | var value = 1; |
作者主要从 ECMASciript 规范讲解 this 的指向,讲解了示例 3 中是如何获取 this 指向的,每次阅读都会有不同的收获,也正如作者所说,希望能打开 this 新世界的大门。
原文链接:JavaScript 深入之从 ECMAScript 规范解读 this
补充下我们使用场景下的 this 指向,如:
1 | var object = {}; |
- 作为对象调用时,指向该对象
obj.b();
,this 指向 obj - 作为函数调用,
var b = obj.b; b();
,this 指向全局 window - 作为构造函数调用,
var b = new Fun();
,this 指向当前实例对象 - 作为 call、apply 调用,
obj.b.apply(object, []);
,this 指向当前的 object - 作为 bind 调用,
var foo = obj.b.bind(object);foo()
,this 永久被绑定到了 object - 作为箭头函数使用,this 与封闭词法环境的 this 保持一致
- 作为DOM 事件处理函数,this 指向触发事件的元素
在严格模式下,如果 this 没有被执行环境(execution context)定义,那它将保持为 undefined。
执行上下文
对于每个执行上下文,都有三个重要属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
分析第一段代码
1 | var scope = "global scope"; |
1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
1 | ECStack = [globalContext]; |
2.全局上下文初始化
1 | globalContext = { |
初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]
1 | checkscope.[[scope]] = [globalContext.VO] |
3.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
1 | ECStack = [checkscope, globalContext]; |
4.checkscope 函数执行上下文初始化:
- 复制函数
[[scope]]
属性创建作用域链, - 用 arguments 创建活动对象,
- 初始化活动对象,即加入形参、函数声明、变量声明,
- 将活动对象压入 checkscope 作用域链顶端。
同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]
.
1 | checkscopeContext = { |
5.执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈
1 | ECStack = [fContext, checkscope, globalContext]; |
6.f 函数执行上下文初始化
1 | fContext = { |
7.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值,这里之所以能够访问到scope
8.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
9.checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
1 | ECStack = [globalContext]; |
分析第二段代码
1 | var scope = "global scope"; |
1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈,并初始化全局上下文
1 | ECStack = [globalContext]; |
初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]
:
1 | checkscope.[[scope]] = [globalContext.VO]; |
2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈,并初始化函数上下文
1 | ECStack = [checkscopeContext, globalContext]; |
同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]
:
1 | f.[[scope]] = [AO, checkscopeContext.AO, globalContext.VO]; |
3.checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
1 | ECStack = [globalContext]; |
4.执行 f 函数,创建 f 函数执行上下文,并压入执行上下文栈,将其初始化
1 | ECStack = [fContext;, globalContext]; |
5.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值。正是因为 checkscope 函数执行上下文初始化时,f 函数同时被创建,保存作用域链到 f 函数的内部属性[[scope]]
,所以即使checkscope函数执行完毕,被弹出执行上下文栈,但是checkscopeContext.AO
依然存在于 f 函数维护的[scope]]
:
1 | fContext = { |
所以,闭包的概念产生了,定义:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
6.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
1 | ECStack = [globalContext]; |
原文链接:JavaScript 深入之执行上下文
闭包
MDN 对闭包的定义为:
闭包是指那些能够访问自由变量的函数。
那什么是自由变量呢?
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
由此,我们可以看出闭包共有两部分组成:
闭包 = 函数 + 函数能够访问的自由变量
ECMAScript中,闭包指的是:
从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
从实践角度:以下函数才算是闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
闭包是指那些能够访问自由变量的函数。
自由变量是指在函数中使用的,但既不是参数也不是函数的局部变量的变量。
那么,闭包 = 函数 + 函数能够访问的自由变量。
看这道刷题必刷,面试必考的闭包题:
1 | var data = []; |
执行data[0](),data[1](),data[2]()
时,i=3,所以都打印 3
让我们改成闭包看看:
1 | var data = []; |
原文链接:JavaScript 深入之闭包
参数按值传递
ECMAScript 中所有函数的参数都是按值传递的。 — 《JavaScript 高级程序设计-第三版》
即,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制给另一个变量一样。
但是通俗地理解,参数如果是基本类型是按值传递,参数如果是引用类型就按共享传递。
共享传递是指,在传递对象的时候,传递对象的引用的副本。
例子一:
1 | var value = 1; |
内存分布如下:
改变前:
栈内存 | 栈内存 | 堆内存 |
---|---|---|
value | 1 | |
v | 1 |
改变后:
栈内存 | 栈内存 | 堆内存 |
---|---|---|
value | 1 | |
v | 2 |
例子二:
1 | var obj = { |
内存分布如下:
改变前:
栈内存 | 栈内存 | 堆内存 |
---|---|---|
obj,o | 指针地址 | {value: 1} |
改变后:
栈内存 | 栈内存 | 堆内存 |
---|---|---|
obj,o | 指针地址 | {value: 2} |
例子三:
1 | var obj = { |
内存分布如下:
改变前:
栈内存 | 栈内存 | 堆内存 |
---|---|---|
obj,o | 指针地址 | {value: 1} |
改变后:
栈内存 | 栈内存 | 堆内存 |
---|---|---|
obj | 指针地址 | {value: 1} |
o | 2 |
以上解释来自:sunsl516 commented on 2 Jun 2017.
原文链接:JavaScript 深入之参数按值传递