前言
还记得毕业那会刚接触 JavaScript
的时候,觉得这门语言很奇怪。和上学时学的 C++
相比,虽然写法差异没那么大,但运行逻辑非常不一样。
比如拷贝下面的JS代码,在浏览器控制台中运行:
console.log(i); // undefined
for (var i = 0; i < 5; i++) {
console.log(i); // 0 1 2 3 4
}
console.log(i); // 5
复制代码
可以看到,变量 i
可以在没有定义的情况下使用,而且打印出了 undefined
并不会报错。中间的打印 0 1 2 3 4
容易理解,但离谱的是,最后一行的打印居然是 5
!
一个没有定义的变量,可以拿来使用且不报错,然后还被for循环内部声明的同名变量影响。
不禁脱口而出一句话:嗯,这才够JS。
我们再来看看 C++ 的代码,拷贝下面的代码到 在线IDE 中运行,会发现,第一行和最后一行的打印都会报错。这是因为对于C++来说,变量需先定义才能使用,而且由于C++中使用了块级作用域,因此for循环内部声明的变量不会影响外部声明的同名变量。
不禁再次脱口而出一句话:这才是我想要的。
// cout<<i
for (int i = 0; i < 5; i++) {
cout<<i;
}
// cout<<i
复制代码
经过上面对比,可能你也发现了,C++
的运行逻辑更符合预期,而 JS
则有点反直觉。那么是不是说明 JS 是一门糟糕的语言呢?
事实上,JS是一门在特定的历史背景下诞生的优秀和糟粕并存的语言。了解更多可以回顾之前的文章 JS概览
好消息是,在形成 ECMAScript 标准后,JS的优秀部分越来越受到重视,而糟粕的部分则越来越被遗弃,这也是JavaScript的发展如此快速的其中一个重要原因。
尽管标准一直在更新,但为了兼容旧代码,JS的执行逻辑几乎没有变,这也是我们学习执行机制的前提。
本文包含以下几个部分:
- 变量提升:JavaScript代码是按顺序执行的吗?
- 调用栈:为什么JavaScript代码会出现栈溢出?
- 块级作用域:var缺陷以及为什么要引入let和const?
- 作用域链和闭包:代码中出现相同的变量,JavaScript引擎是如何选择的?
- 从JavaScript执行上下文的视角讲清楚this
- 知识点梳理和总结
- 整体流程概览
一、变量提升:JavaScript代码是按顺序执行的吗?
要回答这个问题,我们先看一个例子:
console.log(name); // undefined
var name = 'Aaron'
复制代码
上面的代码中,name
使用在声明前,虽然打印出的是 undefined
,但它顺利运行了。而如果是顺序执行的话,解释器找不到这个变量,理应会报错。所以很显然,JavaScript并不是按顺序一行一行来执行的。
其实,JavaScript 的执行机制是 先编译,再执行,即JS执行流程分为两个阶段:编译阶段 和 执行阶段。
- 编译阶段:目标是生成 执行上下文 和 可执行代码 (字节码或机器码)
- 执行阶段:逐行解释执行机器码
执行上下文 可以看成是代码执行所需的环境,包含变量环境 Variable Environment
,用来存储 var
变量。有了这个环境,执行阶段就能顺利获取变量,来完成代码的执行了。
现在我们知道JavaScript是分阶段执行的,那如何解释上面的例子呢?
答案是:变量提升
所谓的变量提升,是指在 JavaScript 代码运行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。
因此,和上面的案例等价的代码如下:
var name = undefined; // 变量提升
console.log(name); // undefined
var name = 'Aaron'
复制代码
小测验:你能按照 JavaScript 的执行流程,来分析最终输出结果吗?
showName()
var showName = function() {
console.log(2)
}
function showName() {
console.log(1)
}
复制代码
这里只是简单介绍了变量提升现象,后面的小节中我们还会介绍JS为什么会有变量提升,以及带来的影响和如何解决。
二、调用栈:为什么JavaScript代码会出现栈溢出?
上面介绍到,JavaScript的执行是先编译后执行的,而且编译阶段会生成执行上下文。这一小节,我们聊聊 执行上下文栈。
下面栈溢出的报错我们都很熟悉,这里的栈其实就是指执行上下文栈,或调用栈:
执行上下文栈是JS引擎用来管理执行上下文的
首先,生成执行上下文的场景有如下几种:
- 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
- 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
- 当使用 eval 函数的时候,eval 的代码也会被编译,并创建eval执行上下文。
当我们执行一段js代码时:
- 首先会先创建全局执行上下文,其中包含全局变量和函数等,并压入栈内;
- 当调用一个函数时,会创建函数执行上下文,其中包含函数内的变量和函数,压入栈内;
- 当函数代码执行完成,则这个函数执行上下文会被销毁,此时仅包含全局执行上下文;
- 最后,当全局代码执行完成,则全局执行上下文也被销毁。
我们可以复制下面的代码到 javascript-visualizer 中,点击 step
按钮一步一步模拟执行过程:
var a = 2
function add(){
var b = 10;
return a+b;
}
add()
复制代码
这一小节介绍了执行上下文栈的作用和执行过程,结合之前的变量提升现象,我们可以通过一个案例稍微解释一下变量提升的过程:
console.log(name); // undefined
var name = 'Aaron'
复制代码
- 首先,在代码的编译阶段会生成全局执行上下文,扫描当前代码中的
var
变量,发现name
,然后,将他加入到执行上下文中的变量环境中,并设置其值为undefined
,此时的变量环境可以理解成{name: undefined}
的结构; - 接着,在代码的执行阶段,当执行到
console.log(name)
时,会到上面生成的执行上下文中查找变量,发现name
为undefined
,因此打印出undefined
; - 最后,执行
var name = 'Aaron'
,这时全局执行上下文中的变量环境可以理解成{name: 'Aaron'}
三、块级作用域:var缺陷以及为什么要引入let和const?
正是由于 JavaScript 存在变量提升这种特性,从而导致了很多与直觉不符的代码,这也是 JavaScript 的一个重要设计缺陷
那么问题来了:
- 为什么在 JavaScript 中会存在变量提升,以及变量提升所带来的问题?
- 如何通过块级作用域并配合 let 和 const 关键字来修复这种缺陷
要回答上面的问题之前,我们先要了解作用域。
作用域
通俗来说,作用域就是变量和函数可访问范围
ES6之前,ES的作用域只有两种:
- 全局作用域:全局可访问,生命周期伴随页面的生命周期
- 函数作用域:函数内部定义的变量或函数,只能在函数内访问。函数执行结束后,函数内部定义的变量会被销毁
其他语言则还支持块级作用域:一对大括号包裹的代码
- 函数
- 判断语句
- 循环语句
- 代码块{}
简单来讲,如果一种语言支持块级作用域,那么其代码块内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁。
为什么会有变量提升?
JS在设计之初,为了简单,没有块级作用域,同时对函数作用域做了变量提升,结果是函数中的变量无论声明在哪里,在编译阶段都会被提升到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都能被访问到,这就是JS中的变量提升。
变量提升带来的问题?
- 变量覆盖
var myname = "极客时间"
function showName(){
console.log(myname);
if(0){
var myname = "极客邦"
}
console.log(myname);
}
showName()
复制代码
- 变量污染(本应销毁的变量没有被销毁)
function foo(){
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
foo()
复制代码
ES6 是如何解决变量提升带来的缺陷?
ES6引入了 let 和 const关键字,从而使得 JavaScript 和其他语言一样拥有了块级作用域。
通过实际的例子来分析下,ES6 是如何通过块级作用域来解决上面的问题的。
function varTest() {
var x = 1;
if (true) {
var x = 2; // 同样的变量!
console.log(x); // 2
}
console.log(x); // 2
}
复制代码
上述代码最后通过 console.log(x)
输出的是 2,而对于相同逻辑的代码,其他语言最后一步输出的值应该是 1,因为在 if 块里面的声明不应该影响到块外面的变量。
既然支持块级作用域和不支持块级作用域的代码执行逻辑是不一样的,那么接下来我们就来改造上面的代码,让其支持块级作用域。
这个改造过程其实很简单,只需要把 var 关键字替换为 let 关键字,改造后的代码如下:
function letTest() {
let x = 1;
if (true) {
let x = 2; // 不同的变量
console.log(x); // 2
}
console.log(x); // 1
}
复制代码
这种就非常符合我们的编程习惯了:作用域块内声明的变量不影响块外面的变量。
JavaScript是如何支持块级作用域的?
块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
复制代码
变量查找路径:当前执行上下文的词法环境 => 当前执行上下文的变量环境 => outer指向的执行上下文中查找
(其中 outer 是执行上下文中的一个指针,它指向外部的执行上下文,后面介绍作用域链时会提到)
通过上面的分析,想必你已经理解了词法环境的结构和工作机制,块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。
思考题: 你能通过分析词法环境,得出来最终的打印结果吗?
let myname= '极客时间'
{
console.log(myname)
let myname= '极客邦'
}
复制代码
四、作用域链和闭包:代码中出现相同的变量,JavaScript引擎是如何选择的?
在上一篇文章中我们讲到了什么是作用域,以及 ES6 是如何通过变量环境和词法环境来同时支持变量提升和块级作用域,在最后我们也提到了如何通过词法环境和变量环境来查找变量,这其中就涉及到作用域链的概念。
这一小节我们就来聊聊什么是作用域链,并通过作用域链再来讲讲什么是闭包。首先我们来看下面这段代码:
function bar() {
console.log(myName)
}
function foo() {
var myName = "极客邦"
bar()
}
var myName = "极客时间"
foo()
复制代码
通过前面几个小节的学习,想必你已经知道了如何通过执行上下文来分析代码的执行流程了。那么当这段代码执行到 bar 函数内部时,其调用栈的状态图如下所示:
从图中可以看出,全局执行上下文和 foo 函数的执行上下文中都包含变量 myName,那 bar 函数里面 myName 的值到底该选择哪个呢?
事实上,上面的代码打印的是 "极客时间" ,也就是 bar 函数中的 myName 取值是全局作用域上的,而不是调用它的 foo 函数中的。
为什么会是这种情况呢?要解释清楚这个问题,那么你就需要先搞清楚作用域链了。
作用域链
每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用叫做 outer 。
当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量。
比如上面那段代码在查找 myName 变量时,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。
为了直观理解,你可以看下面这张图:
从图中可以看出,bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链。
通过作用域查找变量的链条称为 作用域链 。
现在你知道变量是通过作用域链来查找的了,不过还有一个疑问没有解开,foo 函数调用的 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?
要回答这个问题,需要知道什么是 词法作用域 。这是因为在 JavaScript 执行过程中,其作用域链是由词法作用域决定的。
词法作用域
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
这么讲可能不太好理解,你可以看下面这张图:
从图中可以看出,词法作用域就是根据代码的位置来决定的,其中 main 函数包含了 bar 函数,bar 函数中包含了 foo 函数,因为 JavaScript 作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:foo 函数作用域—>bar 函数作用域—>main 函数作用域—> 全局作用域
。
了解了词法作用域以及 JavaScript 中的作用域链,我们再回过头来看看上面的那个问题:在开头那段代码中,foo 函数调用了 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?
这是因为根据词法作用域,foo 和 bar 的上级作用域都是全局作用域,所以如果 foo 或者 bar 函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找。也就是说,词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。
闭包
了解了作用域链,接着我们就可以来聊聊闭包了。关于闭包,理解起来可能会是一道坎,特别是在你不太熟悉 JavaScript 这门语言的时候,接触闭包很可能会让你产生一些挫败感,因为你很难通过理解背后的原理来彻底理解闭包,从而导致学习过程中似乎总是似懂非懂。最要命的是,JavaScript 代码中还总是充斥着大量的闭包代码。
闭包的产生
但理解了变量环境、词法环境和作用域链等概念,那接下来你再理解什么是 JavaScript 中的闭包就容易多了。这里你可以结合下面这段代码来理解什么是闭包:
function foo() {
var myName = "极客时间"
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())
复制代码
根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量,所以当 innerBar 对象返回给全局变量 bar 时,虽然 foo 函数已经执行结束,但是 getName 和 setName 函数依然可以使用 foo 函数中的变量 myName 和 test1。所以当 foo 函数执行完成之后,其整个调用栈的状态如下图所示:
从上图可以看出,foo 函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的 setName 和 getName 方法中使用了 foo 函数内部的变量 myName 和 test1,所以这两个变量依然保存在内存中。这像极了 setName 和 getName 方法背的一个专属背包,无论在哪里调用了 setName 和 getName 方法,它们都会背着这个 foo 函数的专属背包。
之所以是专属背包,是因为除了 setName 和 getName 函数之外,其他任何地方都是无法访问该背包的,我们就可以把这个背包称为 foo 函数的闭包。
闭包的定义
在JavaScript中,根据词法作用域的规则,内部函数总是可以访问外部函数的变量。当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
或者更精简的闭包定义:
内部函数引用外部函数的变量的集合
定义不重要,重要的是理解闭包。
在开发者工具中查看闭包
比如外部函数是 foo ,那么这些变量的集合就成为 foo 函数的闭包。可以在浏览器 console 中 debugger 查看闭包。
闭包是如何形成的?
在函数中返回内部函数(也可能是返回一个包含内部函数的对象)时,JS引擎会提前分析这个内部函数是否引用了外部作用域变量,用来判断是否要创建闭包,有引用的外部变量都不会被gc回收,因此形成闭包。
闭包如何回收?
- 如果引用闭包函数的是一个全局变量,那么闭包会一直存在直到页面关闭;
- 如果引用闭包函数的是一个局部变量,那么等包含这个局部变量的函数销毁后,下次JavaScript引擎执行垃圾回收的时候,判断闭包这块内容如果已经不再被使用了,那么JavaScript引擎的垃圾回收器就会回收这块内存。
闭包的作用
- 保护私有变量
- 维持内部私有变量的状态
应用:函数柯里化
五、从JavaScript执行上下文的视角讲清楚this
在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点(它会根据静态作用域查找上层执行上下文,不符合this的需求),基于这个需求,JavaScript 又搞出来另外一套 this 机制。
因此,作用域链和 this 是两套不同的系统,它们之间基本没太多联系。
JavaScript 中的 this 是什么
事实上,执行上下文中包含:
- 变量环境
- 词法环境
- outer外部环境指针
- this
从图中可以看出,this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。其中:
- 全局执行上下文中的 this 指向 window对象
- 函数执行上下文中的 this 要根据调用的方式来确定指向
this指向
通常,函数执行上下文中的 this 指向是该函数的调用者。例如:
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
}
}
myObj.showThis() // this -> myObj
复制代码
我们也可以设置this的指向:
- 通过函数的 call/apply/bind 方法设置
- 通过对象调用方法设置 (使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的。)
- 通过构造函数中设置(使用new)
this的设计缺陷
嵌套函数中的 this 不会从外层嵌套中继承
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
function bar(){console.log(this)}
bar()
}
}
myObj.showThis()
复制代码
函数 bar 中的 this 指向的是全局 window 对象,而函数 showThis 中的 this 指向的是 myObj 对象
你可以通过一个小技巧来解决这个问题,比如在 showThis 函数中声明一个变量 self 用来保存 this,然后在 bar 函数中使用 self,代码如下所示:
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
var self = this
function bar(){
self.name = "极客邦"
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)
复制代码
执行这段代码,你可以看到它输出了我们想要的结果,最终 myObj 中的 name 属性值变成了“极客邦”。其实,这个方法的的本质是把 this 体系转换为了作用域的体系。
其实,你也可以使用 ES6 中的箭头函数来解决这个问题,结合下面代码:
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
var bar = ()=>{
this.name = "极客邦"
console.log(this)
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)
复制代码
执行这段代码,你会发现它也输出了我们想要的结果,也就是箭头函数 bar 里面的 this 是指向 myObj 对象的。这是因为 ES6 中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数。
通过上面的讲解,你现在应该知道了 this 没有作用域的限制,这点和变量不一样,所以嵌套函数不会从调用它的函数中继承 this,这样会造成很多不符合直觉的代码。要解决这个问题,你可以有两种思路:
- 第一种是把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数。
- 第二种是继续使用 this,但是要把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的 this。
普通函数中的 this 默认指向全局对象 window
上面我们已经介绍过了,在默认情况下调用一个函数,其执行上下文中的 this 是默认指向全局对象 window 的。
不过这个设计也是一种缺陷,因为在实际工作中,我们并不希望函数执行上下文中的 this 默认指向全局对象,因为这样会打破数据的边界,造成一些误操作。如果要让函数执行上下文中的 this 指向某个对象,最好的方式是通过 call 方法来显示调用。
这个问题可以通过设置 JavaScript 的“严格模式”来解决。在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined,这就解决上面的问题了。
this总结
首先,在使用 this 时,为了避坑,你要谨记以下三点:
- 当函数作为对象的方法调用时,函数中的 this 就是该对象;
- 当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;
- 嵌套函数中的 this 不会继承外层函数的 this 值。
最后,我们还提了一下箭头函数,因为箭头函数没有自己的执行上下文,所以箭头函数的 this 就是它外层函数的 this。
箭头函数
引入目的:
- 更简短的函数(各种简化的写法)
- 不绑定this
特点:
- 不绑定this,所以箭头函数的 this 就是它外层函数的 this
- 箭头函数不能用作构造器,和 new一起用会抛出错误。
- 箭头函数没有prototype属性。
知识点梳理和总结
本文涉及到的知识点比较多,有必要进行一下梳理
JS执行过程
- JS执行一段代码分为编译和执行两个阶段,编译阶段生成执行上下文和可执行代码,执行阶段一行一行解释执行
- 执行上下文分为:全局执行上下文、函数执行上下文、eval执行上下文
- 执行上下文中包含:变量环境(存储var变量)、词法环境(存储let、const变量)、this(为了让对象中的函数能使用对象中的变量而引入的机制)、outer指针(指向静态作用域中的上层执行上下文)
- 执行上下文栈是JS引擎用来管理执行上下文的
变量提升
- 变量提升现象:JS执行的编译阶段,会把变量的声明部分和函数的声明部分提升到代码开头的“行为”
- 变量提升是设计缺陷,为了简单,只设计了全局和函数作用域,没有设计块级作用域,因此带来的变量提升会造成变量覆盖、变量污染等问题
- 因此,ES6加入了let和const来解决这个问题,本质是加入了块级作用域
块级作用域
- 块级作用域的原理是:在执行上下文中的词法环境来管理的,词法环境也是一个栈结构,这样块级作用域内部的变量就不会被提升,进而影响到外部环境了
- 有了块级作用域后,变量的查找路径变为:当前执行上下文的词法环境 => 当前执行上下文的变量环境 => outer指向的执行上下文的词法环境、变量环境
- outer : 每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用叫做 outer
- outer和作用域链有关,而js的作用域链是按照词法作用域的规则来的,也就是在代码声明的时候,outer就确定好了的
- 由于词法作用域要求,内部函数需要能访问到外部函数的变量,因此外部函数的变量不能随便被GC,于是有了闭包
闭包
- 闭包可以理解为内部函数引用外部函数变量的集合
- 闭包的产生:是在外部函数返回内部函数时,外部函数执行完毕,外部函数的执行上下文出栈,这时JS引擎会判断返回的这个内部函数是否有引用外部函数内的变量,如果有,则形成闭包,并存在于执行上下文栈中
- 闭包的使用:内部函数被调用,这时会首先在内部函数的执行上下文中查找,如果没有,则向闭包中查找变量
- 闭包的释放:当引用内部函数的变量是全局变量时,和页面同声明周期;当是局部变量时,定义这个局部变量的函数若被销毁,则这个局部变量引用的闭包也会销毁。
- 闭包的作用:是保存私有变量和维护它的状态
this
- this存在于执行上下文中,是被设计用来在对象内部使用对象内部的属性的
- 全局执行上下文中,this指向window
- 函数执行上下文中,this的指向默认指向这个函数,但我们也可以修改它的指向
- 修改方法:apply/call/bind、new、在对象中调用
- this的设计有缺陷,比如嵌套函数的this不会从外层嵌套中继承
- 解决办法是:使用箭头函数、或者定义self(使用作用域链机制代替this)
- 比如普通函数中的this默认指向全局对象window,解决办法是:使用严格模式
箭头函数
- 箭头函数的引入是为了解决this不能继承的缺陷的,同时简化写法
- 箭头函数的特点是它不绑定this、不能当做构造函数new对象,没有prototype属性(构造函数才有)
严格模式
- 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
- 消除代码运行的一些不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的Javascript做好铺垫。