-
Notifications
You must be signed in to change notification settings - Fork 20
Description
前言
前面的一篇博客《JS运行原理》,已经大致讲解过“执行上下文”。这篇博客计划详细说说这个概念。
定义
我们一般把JS引擎运行JS代码分为两步,
- 创建执行上下文
- 逐行执行代码
函数声明和函数表达式都属于函数定义阶段,只有在函数被执行阶段,新的执行上下文才会被创建。
执行上下文确定了第二步需要用的变量(或者说是寻找变量的途径),如果在执行代码的时候无法确定变量将会报错ReferenceError,不包括变量为undefined和null的情况。
执行上下文主要包括三个重要属性:
- 变量对象
- 作用域链
- this
提醒一下,for或if语句不会创建新的执行上下文,函数才会创建新的执行上下文
这篇博客涉及到的概念
- 执行上下文栈
- 提升
- 变量对象
- 作用域链
- 计算this的指向
- 暂时性死区
执行上下文栈
JS引擎刚开始读取JS文件的时候,会创建一个全局上下文,并把它放到执行上下文栈里,之后如果遇到需要新创建执行上下文,会把它压到栈顶;直到相关的代码执行完毕并返回结果,该执行上下文会被出栈。执行上下文栈是创建作用域链的重要参考。
提升(Hoisting)
提升是相对于var声明的变量和函数声明的,函数表达式并不会被提升。
// 函数声明形如:
// name是函数名,类似变量名可以被调用
function name([param,[, param,[..., param]]]) {
[statements]
}
// 函数表达式形如:
// 其中的[name]是函数代名词,只是作为函数体的一个本地变量(意味着外部是不可以调用的),主要用于调用自身
var function_expression = function [name]([param1[, param2[, ..., paramN]]]) {
statements
};
提升的伪逻辑如下:
- 提升var变量,如果存在同名变量(包括同名形参),不进行操作;若不存在,则初始化为undefined
- 提升函数声明,提升并覆盖已有的同名变量
(function (a, b) {
console.log(a, b)
var a;
function b() {};
a = 1;
console.log(a, b)
})(3, 4)
output:
3 [Function: b]
1 [Function: b]
伪代码转换:
(function (a, b) {
var a=3,b=4;
b=function () {}
console.log(a, b)
a = 1;
console.log(a, b)
})(3, 4)
变量对象
全局执行上下文的变量对象有:
- 全局对象(浏览器环境中是window,nodejs中是global),允许直接访问属性(比如经常使用的
new Date,本身就是Window.Date) - 使用或不使用var定义的变量,差别是不使用var定义的变量可以用delete操作符删除
- 函数声明
函数执行上下文的变量对象有:
- arguments
- 实参
- 使用var定义的变量
- 函数声明
作用域链
当在本执行上下文中的变量对象中没有找到目标变量的话,就会借助作用域来查找变量。
比如:在函数执行上下文中未使用var定义的变量被调用的时候,因为在变量对象中找不到,所以会顺着作用域链查找下一个执行上下文中的变量对象。
作用域链是根据执行上下文栈来确定的。
特殊作用域:
- try-catch语句块,临时在作用域链中添加error对象
- with语句块,也会临时在作用域链中添加with的对象
- eval语句,如果你间接的使用 eval(),比如通过一个引用来调用它,而不是直接的调用 eval ,从 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中
计算this的指向
一旦确定了this的指向,当访问this的某个成员变量或函数,如果不存在不会根据执行上下文栈找到上一个this并查询。
1. 全局执行上下文
this指向全局对象,在浏览器环境下是window,在nodejs下是global
2. 函数执行上下文
- 作为对象的方法,即使方法是来自该对象的原型链上的方法,this也指向该对象
- 作为构造函数,指向正在构建的新对象
- 如果是简单调用,就是没有形如
fun()而不是obj.fun(),this指向全局对象,但是在严格模式下,会指向undefined - 箭头函数,与封闭词法上下文的this保持一致
这里的封闭词法上下文没有那么高大上,说白了,箭头函数的执行上下文没有定义this属性,根据执行上下文栈找到上一个执行上下文的this,就是箭头函数中的this。下面看一段代码。
var x = 'windows';
function fun1() {
var fun2 = () => {
console.log(this.x)
}
fun2()
}
fun1();
var obj = {
x: 'obj',
showX: fun1
}
obj.showX()
浏览器非严格模式的output:
windows
obj
nodejs,默认就是严格模式:
undefined
obj
fun2的上一个执行上下文就是fun1,所以fun2的this就是fun1的this。第一次调用是简单调用,所以fun1的this指向全局变量(window);第二次调用是作为对象的方法被调用,所以this指向新对象,而新对象里刚好有x变量。
3. 特殊情况
- 使用apply和call传递this指向
- 使用bind方法绑定this的指向,只对第一次的绑定生效
- DOM事件构造函数里的this指向触发事件的DOM元素
- 内联事件处理函数里的this指向监听器所绑定的DOM元素
暂时性死区
只有在es6中的let和const才会发生这种情况。
进入一个执行上下文的时候,
let、const声明的变量和形参定义的变量,都会屏蔽掉前一个执行上下文中的同名变量,
且在声明语句之前是不可以使用该变量,
声明语句之后,若该变量没有被赋值,则默认为undefined。
var x = 0;
function f(x = x) { // A行
console.log(x)
console.log(y) // B行
let y = 1;
}
f()
output:
ReferenceError: x is not defined
ReferenceError: y is not defined
上面的代码里A行中的第一个x是形参定义的变量,它会屏蔽掉全局中的x,所以会报错;B行中的y由于是let定义的变量,所以在声明语句前调用将会报ReferenceError错误。
参考链接
http://yanhaijing.com/javascript/2014/04/29/what-is-the-execution-context-in-javascript/
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval
https://www.cnblogs.com/snandy/archive/2011/03/19/1988284.html
http://blog.csdn.net/yangbingbinga/article/details/61424363