JavaScript 代码是如何执行的?
当面试官给你一段代码让你告诉他结果,这个时候我们的大脑就要负责去运行这些代码。因此深入了解代码运行的规则是学习一门语言基础中的基础。只有很好的掌握了代码运行规则才能减少平时代码的bug。
先看一张图让我们对这些全新的概念有一个宏观的认识。
很明显,有一个非常重要的概念“执行上下文”,我们就先从它开始讲解。
执行上下文
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行
我们先看一段代码:
var name = "jack";
var age = 18;
function foo(i){
var bar = "bar";
function f1(){
console.log('f1');
}
f1(); // 'f1'
console.log(i,name,age); // 10 "jack" 18
}
foo(10);
过程分析:
1、初始化时有一个空的执行环境栈
executionContextStack = []
2、 初始化时会产生一个全局上下文globalExecutionContext(全局上下文有且只有一个)
globalExecutionContext = {
VO: {
name: undefined,
age: undefined,
foo: function foo(i){...}
},
scopeChain: [],
this: { ... }
}
此时执行环境栈的值为:
executionContextStack = [globalExecutionContext]
3、执行全局代码时
globalExecutionContext = {
AO: {
name: "jack",
age: 18,
foo: function foo(i){...}
},
scopeChain: [AO],
this: { ... }
}
4、代码执行到 foo(i){...} 定义时,并且发现有调用函数的代码 foo(10);(还未执行foo(10)前)
先创建一个上下文对象
- 创建arguments对象,检查当前上下文的参数,建立该对象下的属性和属性值
- 扫描上下文的函数申明,指向该函数在内存中的地址(如果函数名在VO中已经存在,对应的属性值会被新的引用覆盖)
- 扫描上下文的变量申明,初始化为undefined(如果该变量名在VO中已经存在,则直接跳过继续扫描)
- 初始化作用域链
- 确定上下文中this的指向
fooExecutionContext = {
VO: {
arguments: {
0: 10,
length: 1
},
i: 10,
f1: pointer to function f1(),
bar: undefined
},
scopeChain: [globalExecutionContext.AO],
this: { ... }
}
此时执行环境栈的值为:
executionContextStack = [fooExecutionContext,globalExecutionContext]
4、代码执行foo(10)时
- 执行
foo函数体中的代码,给VO中的变量赋值 - 同时扫描到
f1函数并且该函数有相应的调用f1();此时会创建f1函数上下文
第一步:给VO中的变量赋值
fooExecutionContext = {
AO: {
arguments: {
0: 10,
length: 1
},
i: 10,
f1: pointer to function f1(),
bar: "bar"
},
scopeChain: [AO,globalExecutionContext.AO],
this: { ... }
}
foo函数内部需要使用变量name 与 age 时会在scopeChain作用域链中一级一级的查找,直到找到为止,如果没有找到则抛出错误。
第二步:创建f1函数上下文
f1ExecutionContext = {
variableObject: {
arguments: {
length: 0
}
},
scopeChain: [fooExecutionContext.AO,globalExecutionContext.AO],
this: { ... }
}
此时执行环境栈的值为:
executionContextStack = [f1ExecutionContext,fooExecutionContext,globalExecutionContext]
5、执行f1函数调用
f1ExecutionContext = {
AO: {
arguments: {
length: 0
}
},
scopeChain: [AO,fooExecutionContext.AO,globalExecutionContext.AO],
this: { ... }
}
6、f1 函数执行完毕,f1 函数上下文从执行上下文栈中弹出
此时执行环境栈的值为:
executionContextStack = [fooExecutionContext,globalExecutionContext]
7、foo 函数执行完毕,foo 函数上下文从执行上下文栈中弹出
此时执行环境栈的值为:
executionContextStack = [globalExecutionContext]
8、当浏览器关闭时清空执行环境栈executionContextStack = []
小结:
- 调用函数时会为其创建执行上下文,并压入执行环境栈的栈顶,执行完毕 弹出,执行上下文被销毁,随之VO也被销毁
- 执行上下文分创建阶段和代码执行阶段
- 创建阶段初始变量值为undefined,执行阶段才为变量赋值
- 函数申明先于变量申明
【特别说明】以上列举的执行上下文对象是ES3时的规范,为什么要用那么早的规范来讲呢,原因就是简单方便理解。我们也只是理解执行过程,并不需要完完整整去理解执行的每一个细节。
执行上下文在 ES3 中,包含三个部分
- scope:作用域,也常常被叫做作用域链。
- variable object:变量对象,用于存储变量的对象。
- this value:this 值。
在 ES5 中,我们改进了命名方式,把执行上下文最初的三个部分改为下面这个样子。
- lexical environment:词法环境,当获取变量时使用。
- variable environment:变量环境,当声明变量时使用。
- this value:this 值。
在 ES2018 中,执行上下文又变成了这个样子,this 值被归入 lexical
- environment,但是增加了不少内容。
- lexical environment:词法环境,当获取变量或者 this 值时使用。
- variable environment:变量环境,当声明变量时使用。
- code evaluation state:用于恢复代码执行位置。
- Function:执行的任务是函数时使用,表示正在被执行的函数。
- ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
- Realm:使用的基础库和内置对象实例。
- Generator:仅生成器上下文有这个属性,表示当前生成器。
词法作用域和动态作用域
作用域链是一个相对比较好理解的概念,但想要完全理解作用域链,就必须得理解词法作用域和动态作用域。
JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。
而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
词法作用域:
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
动态作用域:
执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。
由于 JavaScript 是采用词法作用域(也可叫静态作用域)因此输出是1。
再来看一个《JavaScript权威指南》中的例子:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
两段代码都会打印:local scope。原因也很简单,因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。
从执行环境栈的角度来分析:
第一个:
executionContextStack.push('checkscope 上下文');
executionContextStack.push('f 上下文');
ECStack.pop(); // 弹出 'f 上下文'
ECStack.pop(); // 弹出 'checkscope 上下文'
第二个:
executionContextStack.push('checkscope 上下文');
ECStack.pop(); // 弹出 'checkscope 上下文'
executionContextStack.push('f 上下文');
ECStack.pop(); // 弹出 'f 上下文'
虽然获取到的结果是一致的,但是计算机的处理过程却是不一样的。
this
JavaScript 的 this 总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。
除去不常用的with和eval的情况,具体到实际应用中,this的指向大致可以分为以下4种。
- 作为对象的方法调用。
- 作为普通函数调用。
- 构造器调用。
- Function.prototype.call或Function.prototype.apply调用。
作为对象的方法调用
var obj = {
a: 1,
getA: function(){
alert ( this === obj ); // 输出:true
alert ( this.a ); // 输出: 1
}
};
obj.getA();
当函数作为对象的方法被调用时,this指向该对象
作为普通函数调用
window.name = 'globalName';
var getName = function(){
return this.name;
};
console.log( getName() ); // 输出:globalName
当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的this总是指向全局对象。在浏览器的JavaScript里,这个全局对象是window对象。
嵌套的函数独立调用时,this默认绑定到window
<html>
<body>
<div id="div1">我是一个div</div>
</body>
<script>
window.id = 'window';
document.getElementById( 'div1' ).onclick = function(){
alert ( this.id ); // 输出:'div1'
var callback = function(){
alert ( this.id ); // 输出:'window'
}
callback();
};
</script>
</html>
解决方案:使用变量保存this
document.getElementById( 'div1' ).onclick = function(){
var that = this; // 保存div的引用
var callback = function(){
alert ( that.id ); // 输出:'div1'
}
callback();
};
严格模式下this不指向全局对象
function func(){
"use strict"
alert ( this ); // 输出:undefined
}
func();
构造器调用
当用new运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的this就指向返回的这个对象
var MyClass = function(){
this.name = 'sven';
};
var obj = new MyClass();
alert ( obj.name ); // 输出:sven
但用new调用构造器时,还要注意一个问题,如果构造器显式地返回了一个object类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的this
var MyClass = function(){
this.name = 'sven';
return { // 显式地返回一个对象
name: 'anne'
}
};
var obj = new MyClass();
alert ( obj.name ); // 输出:anne
call 和 apply 调用
var obj1 = {
name: 'sven',
getName: function(){
return this.name;
}
};
var obj2 = {
name: 'anne'
};
console.log( obj1.getName() ); // 输出: sven
console.log( obj1.getName.call( obj2 ) ); // 输出:anne
call 和 apply 可以动态地改变传入函数的this。
以上便是this的使用总结。从执行上下文的角度来看待this的话,那么是当编译器扫描到函数的调用创建ExecutionContext时就确定好了,并且保存在上下文对象中。