跳到主要内容

语言特性

作用域和作用域链

运行期上下文:当函数执行时,会创建一个称为执行上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行上下文被销毁。

执行上下文包含三个部分:

  • 变量对象(VO,存储着变量和函数声明);
  • 作用域链;
  • this 指向

作用域:上下文中声明的变量和函数的作用范围。分为块级作用域和函数作用域。

每个 JavaScript 函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供 JavaScript 引擎存取,[[scope]] 就是其中之一。

[[scope]] 指的是我们所说的作用域,其中存储了执行期上下文的集合。

作用域链[[scope]] 中存储的执行期上下文对象的集合,这个集合呈链式调用,我们把这种链式链接叫做作用域链。

JavaScript 中的作用域就是词法作用域。词法作用域 是一套关于引擎如何寻找变量以及在何处找到变量的规则。词法作用域最重要的特征是它的定义过程发生在代码的书写阶段(假设你没有使用 eval()with)。词法作用域是由书写代码时函数声明的位置决定的。

函数作用域

JavaScript 具有基于函数的作用域,属于这个函数的全部变量都可以在整个函数的范围内使用。

动态作用域

动态作用域并不关心函数和作用域是如何声明以及在何处声明的,它只关心 从何处调用。即:作用域链是基于调用栈的,而不是代码的作用域嵌套。

事实上 JavaScript 并不具有动态作用域,它只有词法作用域。但是 this 机制在某种程度上很像动态作用域。

块作用域

ES6 之前,可以使用 withcatch 或者立即执行函数(IIFE)的方式模拟块作用域,例如:

try{
throw 1;
}catch(e){
console.log(e); // 1
}
console.log(e); // 会报错

(function(){
// ...
})()

ES6 中引入了 let,可以创建完整的、不受约束的块作用域。let 关键字可以将变量绑定到所在的任意作用域中(通常是 {...} 内部)。即:let 为其声明的变量隐式地劫持了所在地块作用域。

代码执行过程

  • 创建全局上下文;
  • 全局执行上下文逐行自上而下执行,遇到函数时,函数执行上下文被 push 到执行栈顶层;
  • 函数执行上下文被激活,开始执行函数中的代码,全局执行上下文被挂起;
  • 函数执行完毕,函数执行上下文 pop 移除出执行栈,控制权交还给全局上下文,继续执行下面的代码。

闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

let const 与 var 的区别

letconst 的特性相似。两者与 var 的不同之处:

  • var 声明的变量会进行变量提升,letconst 不会变量提升,它们只在所在的代码块内有效(即 {} 内),提前获取 let 或者 const 声明的变量的值会报错,提前使用 var 声明的变量,值会是 undefined
  • var 可以重复声明变量,但 letconst 不能重复声明,这会报错。var 声明之后再使用 let 或者 const 声明,或者用 letconst 声明之后再使用 var 重复声明也会报错;
  • 只要块级作用域内存在 let 命令,它所声明的变量就“绑定”这个区域,不再受外部影响。在代码内,使用 let 声明变量之前,改变量是不可用的,在语法上,称为“暂时性死区”(temporal dead zone,TDZ)。
  • let 实际上为 JavaScript 新增了块级作用域。

const 声明一个只读的常量,一旦声明,常量的值就不能改变。

块级作用域

ES6 之前,可以使用立即执行函数(IIFE)创建块级作用域。ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于 let,在块级作用域之外不可引用。

function fn(){
console.log('World!');
}
(function(){
if(false){ // 创建了块级作用域
function fn(){ // 重复生命一次 fn 函数
console.log('Hello!');
}
}
fn(); // 调用会报错!
// fn is not a function
})();

在符合 ES6 的浏览器中,上面都会报错,因为实际运行的是以下的代码:

function fn() {
console.log('World!');
}
(function () {
var fn = undefined;
if (false) { // 创建了块级作用域
function fn() { // 重复生命一次 fn 函数
console.log('Hello!');
}
}
fn(); // 调用会报错!
})();

原型链

对象之间通过原型关联到一起,就好比用一条锁链将一个个对象连接在一起,在与各个对象挂钩后,最终形成一条原型链。在读取对象的一个属性时,会先在对象中查询自有属性,如果不存在,那么会沿着原型链向上搜索匹配的继承属性,直至找到或到达原型链顶端,才停止搜索。

this 指向

this 关注函数如何调用。this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时地各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式(而不是声明位置)。

非严格模式下,this 默认绑定到 window 上,严格模式下 this 会绑定到 undefined

箭头函数不能显示地绑定 this,即用 callapplybind 绑定 this 时,箭头函数会忽略第一个参数。

箭头函数并不关心 this 绑定。在箭头函数中使用 this 时,使用词法作用域中的 this。

undefined 和 null 的异同

相同部分:

  1. 不包含方法或属性;
  2. 都是假值;
  3. 都只有一个值;
  4. 都是“空缺”的意思;

不同之处:

  1. 含义不同,undefined 表示一个未定义的值,null 表示一个空的对象;
  2. 类型不同,typeof undefined 会得到 'undefined';而 typeof null 会得到 object
  3. 数字转换不同,Number(undefined) 会得到 NaN;而 Number(null) 会得到 0
  4. 在非严格模式下,undefined 能被当作变量来使用和赋值,而 null 不行。

空对象的强制类型转换

考虑下面代码,会打印出什么?

[] + {} = ?
{} + [] = ?
{} + 2 = ?

答案:

'[object Object]'
0
2

第一个比较好理解,[] 会转成字符串与 {} 相加,{} 也会转成字符串:

[] => ""    // [] 转成空字符串
{} => '[object Object]'

第二个结果是数字 0。这是因为 {} 在表达式左侧时表示不执行任何操作的空代码块, + [] 相当于 +[]+A 运算会尝试将 A 转成数字。[] 转成数字是 0

明白了第二个运算结果的由来,第三个语句也就明白了为什么是 2

相关文档:

JavaScript 中的相等性判断

需要注意的是,如果把运算式用 console.log 包裹, + [] 将变成 '[object Object]'{} 将不认为是空代码块,它会调用对象的 toString 方法,转成字符串。