作用域:
作用域(Scope)即代码执行过程中的变量、函数或者对象的可访问区域,作用域决定了变量或者其他资源的可见性;计算机安全中一条基本原则即是用户只应该访问他们需要的资源,而作用域就是在编程中遵循该原则来保证代码的安全性。除此之外,作用域还能够帮助我们提升代码性能、追踪错误并且修复它们。JavaScript 中的作用域主要分为全局作用域(Global Scope)与局部作用域(Local Scope)两大类,在 ES5 中定义在函数内的变量即是属于某个局部作用域,而定义在函数外的变量即是属于全局作用域。
执行上下文:
每个执行上下文又会分为内存创建(Creation Phase)与代码执行(Code Execution Phase)两个步骤,在创建步骤中会进行变量对象的创建(Variable Object)、作用域链的创建以及设置当前上下文中的 this 对象。所谓的 Variable Object ,又称为 Activation Object,包含了当前执行上下文中的所有变量、函数以及具体分支中的定义。当某个函数被执行时,解释器会先扫描所有的函数参数、变量以及其他声明:
'variableObject': {
// contains function arguments, inner variable and function declarations
}
在 Variable Object 创建之后,解释器会继续创建作用域链(Scope Chain);作用域链往往指向其副作用域,往往被用于解析变量。当需要解析某个具体的变量时,JavaScript 解释器会在作用域链上递归查找,直到找到合适的变量或者任何其他需要的资源。作用域链可以被认为是包含了其自身 Variable Object 引用以及所有的父 Variable Object 引用的对象:
'scopeChain': {
// contains its own variable object and other variable objects of the parent execution contexts
}
而执行上下文则可以表述为如下抽象对象:
executionContextObject = {
'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts
'variableObject': {}, // contains function arguments, inner variable and function declarations
'this': valueOfThis
}
变量的生命周期与提升:
变量的生命周期包含着变量声明(Declaration Phase)、变量初始化(Initialization Phase)以及变量赋值(Assignment Phase)三个步骤;其中声明步骤会在作用域中注册变量,初始化步骤负责为变量分配内存并且创建作用域绑定,此时变量会被初始化为 undefined,最后的分配步骤则会将开发者指定的值分配给该变量。传统的使用 var 关键字声明的变量的生命周期如下:
函数的生命周期与提升:
基础的函数提升同样会将声明提升至作用域头部,不过不同于变量提升,函数同样会将其函数体定义提升至头部;
在内存创建步骤中,JavaScript 解释器会通过 function 关键字识别出函数声明并且将其提升至头部;函数的生命周期则比较简单,声明、初始化与赋值三个步骤都被提升到了作用域头部:
function b() {
a = 10;
return;
function a() {}
}
会被编译器修改为如下模式:
function b() {
function a() {}
a = 10;
return;
}
而 JavaScript 中提供了两种函数的创建方式,函数声明(Function Declaration)与函数表达式(Function Expression);函数声明即是以 function 关键字开始,跟随者函数名与函数体。而函数表达式则是先声明函数名,然后赋值匿名函数给它; 函数表达式遵循变量提升的规则,函数体并不会被提升至作用域头部:
sayHello();
function sayHello () {
function hello () {
console.log('Hello!');
}
hello();
var hello = function () {
console.log('Hey!');
}
}
// Hello!
在 ES5 中,是不允许在块级作用域中创建函数的;而 ES6 中允许在块级作用域中创建函数,块级作用域中创建的函数同样会被提升至当前块级作用域头部与函数作用域头部。不同的是函数体并不会再被提升至函数作用域头部,而仅会被提升到块级作用域头部:
f; // Uncaught ReferenceError: f is not defined
(function () {
f; // undefined
x; // Uncaught ReferenceError: x is not defined
if (true) {
f();
let x;
function f() { console.log('I am function!'); }
}
}());