一. ECMA新描述概念
1. 以前的描述
- 在执行学习
JS
代码执行过程中,我们学习了很多ECMA
文档的术语:- 执行上下文栈(
ECS
):Execution Context Stack
,用于执行上下文的栈结构 - 执行上下文(
EC
):Execution Context
,代码在执行之前会先创建对应的执行上下文 - 变量对象(
VO
):Variable Object
,上下文关联的VO
对象,用于记录函数和变量声明 - 全局对象(
GO
):Global Object
,全局执行上下文所关联的VO
对象 - 激活对象(
AO
):Activation Object
,函数执行上下文所关联的VO
对象 - 作用域链:
scope chain
,作用域链,用于关联执行上下文的变量查找
- 执行上下文栈(
- 在新的
ECMA
代码执行描述中(ES6
以及之上),对于代码的执行流程描述改成了另外的一些词汇:- 基本思路是相同的,只是对于一些词汇的描述发生了改变
- 执行上下文栈和执行上下文也是相同的
2. 词法环境(lexical Environments)
词法环境是一种规范类型,用于在词法嵌套结构中定义关联的变量、函数等标识符
- 一个词法环境是由环境记录(
Environment Record
)和一个外部词法环境(Outer Lexical Environment
)组成 - 一个词法环境经常用于关联一个函数声明、代码块语句、
try-catch
语句,当它们的代码被执行时,词法环境被创建出来
a lexical environment is a specification type used to define the association of identifiers to specific variables and functions based upon the lexical nesting structure or ECMAScript code.a lexical environment consists of an environment record and a possibly null reference to an outer lexical environment. usually a lexical environment is associated width some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a Try Statement and a new Lexical Environment is created each time such code is evaluated
词法环境是一种规范类型,用于根据词法嵌套结构或
ECMAScript
代码定义标识符与特定变量和函数的关联。 词汇环境由一个环境记录和一个可能为空的外部词汇环境引用组成。 通常词法环境与ECMAScript
代码的一些特定的语法结构相关联,如函数声明、代码块语句或Try-catch
语句,每次计算这些代码时都会创建一个新的词法环境
- 一个词法环境是由环境记录(
也就是在
ES5
之后,执行一个代码,通常会关联对应的词法环境:那么执行上下文会关联哪些词法环境呢?
ECMAScript
代码执行上下文的附加状态组件:- 词法环境(
LexicalEnvironment
):标识用于解析此执行上下文中代码所做的标识符引用的词法环境 - 变量环境(
VariableEnvironment
):标识词法环境,它的环境记录保存由变量语句在执行上下文中创建的绑定
- 词法环境(
3. LexicalEnvironment和VariableEnvironment
LexicalEnvironment
用于处理let
、const
声明的标识符:let and const declarations define variables that are scoped to the running execution context's lexical Environment. the variables are created when their containing lexical environment is instantiated but may not be accessed in any way until the variable's lexical binding is evaluated. a variable defined by a lexical binding with an initializer is assigned the value of its initializer's assignment Expression when the lexical binding is evaluated, not when the variable is created. if a lexical binding in a let declaration does not have an initializer the variable is assigned the value undefined when the lexical binding is evaluated
let
和const
声明定义了作用域为运行执行上下文的词法环境的变量。 变量在其包含的词法环境被实例化时被创建,但在变量的词法绑定被求值之前不能以任何方式访问。 词法绑定和初始化式定义的变量在词法绑定计算时赋值给它的初始化式赋值表达式,而不是在变量创建时赋值。 如果let
声明中的词法绑定没有初始化器,那么在计算词法绑定时,变量将被赋值为undefined
VariableEnvironment
用于处理var
和function
声明的标识符:a var statement declares variables that are scoped to the running execution context's variable environment. var variables are created when their containing lexical environment is instantiated and are inittialized to undefined when created. within the scope of any variable environment a common binding identifier may appear in more than one variable declaration but those declarations collectively define only one variable. a variable defined by a variable declaration with an initializer is assigned the value of its initializer's assignment expression when the variable declaration is executed, not when the variable is created
var
语句声明作用域为正在运行的执行上下文的变量环境的变量。Var
变量在其包含的词法环境被实例化时被创建,并在创建时初始化为未定义。 在任何变量环境的作用域内,一个通用的绑定标识符可能出现在多个变量声明中,但是这些声明合起来只定义了一个变量。 使用初始化时的变量声明定义的变量在执行变量声明时赋值给初始化式的赋值表达式,而不是在创建变量时赋值
4. 环境记录(Environment Record)
在这个规范中有两种主要的环境记录值:声明式环境记录 和 对象环境记录
声明式环境记录:用于定义
ECMAScript
语言语法元素的效果,如函数声明、变量声明和直接将标识符绑定与ECMAScript
语言值关联起来的Catch
子句js// 声明式 var a = 1 let b = 2
对象式环境记录:对象环境记录用于定义
ECMAScript
元素的效果,例如With
语句,它将标识符绑定与某些对象的属性关联起来js// 对象式 with(obj) { console.log(obj) }
there are two primary kinds of environment record values used in this specification: declarative environment records and object environment records. declarative environment records are used to define the effect of ECMAScript language syntactic elements such as function declarations, variable declarations, and catch clauses that directly associate identifier bindings with ECMAScript language values. object environment records are used to define the effect of ECMAScript elements such as with statement that associate identifier bindings with the properties of some object. global environment records and function environment records are specializations that are used for specifically for script global declarations and for toplevel declarations within functions
- 该规范中使用了两种主要的环境记录值: 声明性环境记录和对象环境记录。 声明性环境记录用于定义
ECMAScript
语言语法元素的效果,如函数声明、变量声明和直接将标识符绑定与ECMAScript
语言值关联起来的catch
子句。 对象环境记录用于定义ECMAScript
元素的效果,例如with
语句,该语句将标识符绑定与某些对象的属性关联起来。 全局环境记录和函数环境记录是专门用于脚本全局声明和函数中的顶级声明的专门化
5. 新ECMA描述内存图

二. let、const的使用
- 在
ES5
中我们声明变量都是使用的var
关键字,从ES6
开始新增了两个关键字可以声明变量(声明标识符):let、const
let、const
在其他编程语言中都是有的,所以也并不是新鲜的关键字- 但是
let
、const
确确实实给js
带来一些不一样的东西
let
关键字:- 从直观的角度来说,
let
和var
是没有太大的区别的,都是用于声明一个变量(标识符)
- 从直观的角度来说,
const
关键字:const
关键字是constant
的单词的缩写,表示常量、衡量的意思- 它表示保存的数据一旦被赋值,就不能被修改
- 但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容
三. let、const和var的区别
1. let、const不允许重复声明变量
let、const
不允许重复声明变量(标识符),会抛出错误var
是允许重复声明一个变量(标识符),这其实算是JS
早期设计上的一个缺陷BindingIdentifier
: 绑定标识符Initializer
:初始化程序AssignmentExpression
:赋值表达

2. let、const没有作用域提升
let
、const
和var
的另一个重要区别是作用域提升:- 我们知道
var
声明的变量是会进行作用域提升的 - 使用
var
声明的变量将在任何代码执行前被创建,这被称为 变量提升。这些变量的初始值为undefined
- 从概念的字面意义上说,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,但这么说并不准确。实际上变量和函数声明在代码里的位置是不会动的,而是在编译阶段被放入内存中
- 函数和变量相比,会被优先提升。这意味着函数会被提升到更靠前的位置
- 但是如果我们使用
let
声明的变量,在声明之前访问会报错
jsconsole.log(a) // undefined console.log(b) // Uncaught ReferenceError: Cannot access 'b' before initialization var a = 1 let b = 2
- 我们知道
那么是不是意味着
b
变量只有在代码执行阶段才会创建的呢?- 事实上并不是这样的,我们可以看一下
ECMA262
对let
和const
的描述 - 这些变量会被创建在包含他们的词法环境被实例化时,但是不可以访问它们,直到词法绑定被求值
- 事实上并不是这样的,我们可以看一下
从上面我们可以看出,在执行上下文的词法环境创建出来的时候,变量在内存中其实已经被创建了,只是这个变量是不能被访问的
- 那么变量已经有了,但是不能被访问,是不是一种作用域的提升呢?
事实上维基百科并没有对作用域提升有严格的概念解释,那么我们自己从字面量上理解
- 作用域提升:在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升
- 在这里,它虽然被创建出来了,但是不能被访问,也就没什么意义,所以个人认为不能称之为作用域提升
所以个人的观点是
let
、const
没有进行作用域提升,但是标识符会在解析阶段被提前创建出来
3. 暂时性死区(TDZ)
我们知道,在
let
、const
定义的标识符真正执行到声明的代码之前,是不能被访问的从块作用域的顶部一直到变量声明完成之前,这个变量处在暂时性死区(
TDZ,temporal dead zone
)官方是没有这个术语的,这玩意是社区讨论出来的名字
js{ console.log(bar) // Uncaught ReferenceError: Cannot access 'bar' before initialization let bar = 'hello' }
暂时性死区和定义的位置没有关系,和代码的执行顺序有关
jsfunction foo() { console.log(msg) } let msg = 'message' foo() // message
暂时性死区形成之后,在该区域内
let
、const
定义的标识符不能被访问jsvar msg = 'hello' function foo() { console.log(msg) let msg = 'world' } foo() // Uncaught ReferenceError: Cannot access 'msg' before initialization
4. let、const不添加到window
我们知道,在全局通过
var
来声明一个变量,事实上会在window
上添加一个属性:- 但是
let
、const
是不会给window
上添加任何属性的
- 但是
那么我们可能会想这个变量是保存在哪里呢?
let
、const
声明的变量是添加到声明性环境记录(Declarative Environment Record
)中的
四. 块级作用域的使用
1. ES6之前的作用域
在
es6
之前,只有全局和函数会形成自己的作用域ES5
中,一个代码块中定义的变量,外面是可以访问的:
2. let和const的块级作用域
在
ES6
中新增了块级作用域,并且通过let
、const
、function
、class
声明的标识符是具备块级作用域的限制的:jsfoo() // 不能访问 { let a = 'aaa' const b = 'bbb' function foo() { console.log('foo') } class Person {} } console.log(a) // 不能访问 console.log(b) // 不能访问 foo() // 可以访问 var p = new Person() // 不能访问
如上:官方标准中是函数是拥有块级作用域的,但是外面依然是可以访问的:
- 这是因为引擎会对函数的声明进行特殊的处理,允许像
var
一样在外界访问,但只能是在函数声明代码执行之后
- 这是因为引擎会对函数的声明进行特殊的处理,允许像
3. 块级作用域的应用
当执行完
let、const
所在的代码块中的代码时,全局的词法环境会重新指回全局的词法环境,而let\const
所在代码块的词法环境,这时从全局出发没有引用指向它,则会被内存回收掉html<button>按钮1</button> <button>按钮2</button> <button>按钮3</button> <button>按钮4</button> <script> const btnEls = document.querySelectorAll('button') // for (var i = 0; i < btnEls.length; i++) { // btnEls[i].onclick = function() { // console.log(`点击了第${i+1}个按钮`) // } // } // for (var i = 0; i < btnEls.length; i++) { // btnEls[i]._index = i+1 // btnEls[i].onclick = function() { // console.log(`点击了第${this._index}个按钮`) // } // } // for (var i = 0; i < btnEls.length; i++) { // (function(i) { // btnEls[i].onclick = function() { // console.log(`点击了第${i+1}个按钮`) // } // })(i) // } for (let i = 0; i < btnEls.length; i++) { // 这里每次循环的时候,都会形成一个新的词法环境,会保留下来对应的i btnEls[i].onclick = function() { // 这里将函数对象绑定给了dom元素身上,不会被销毁 console.log(`点击了第${i+1}个按钮`) // 函数对象通过闭包的形式引用着外层词法环境中环境记录的i,所以该词法环境也不会被销毁,然后点击的时候,通过查找函数对象的作用域链,找到外层块级作用域中的i } } </script>
4. var、let、const的选择
- 对于
var
的使用:- 我们需要明白一个事实,
var
所表现出来的特殊性:比如作用域提升、window
全局对象、没有块级作用域等都是一些历史遗留问题 - 其实是
JavaScript
在设计之初的一种语言缺陷 - 当然目前市场上也在利用这种缺陷出一系列的面试题,来考察大家对
JavaScript
语言本身以及底层的理解 - 但是在实际工作中,我们可以使用最新的规范来编写,也就是不再使用
var
来定义变量了
- 我们需要明白一个事实,
- 对于
let
、const
:- 对于
let
和const
来说,是目前开发中推荐使用的 - 我们会优先推荐使用
const
,这样可以保证数据的安全性不会被随意的篡改 - 只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用
let
- 这种在很多其他语言里面也都是一种约定俗成的规范,尽量我们也遵守这种规范
- 对于
五. 模板字符串的详解
1. 模板字符串基本使用
在
ES6
之前,如果我们想要将字符串和一些动态的变量(标识符)拼接到一起,是非常麻烦和丑陋的ugly
ES6
允许我们使用字符串模板来嵌入JS
的变量或者表达式来进行拼接:- 首先,我们会使用
``
符号来编写字符串,称之为模板字符串 - 其次,在模板字符串中,我们可以通过
${expression}
来嵌入动态的内容
jsconst nickName = 'later' const age = 18 console.log(`my name is ${nickName}, age is ${age}`) // my name is later, age is 18 console.log(`我${age >= 18 ? '是' : '不是'}成年人`) // 我是成年人 function foo() { return 'foo' } console.log(`调用了${foo()}函数`) // 调用了foo函数
- 首先,我们会使用
2. 标签模板字符串使用
模板字符串还有另外一种用法:标签模板字符串(
Tagged Template Literals
)我们一起来看一个普通的
JS
函数:jsfunction foo(...args) { console.log(args) } foo('hello world') // ['hello world']
如果我们使用标签模板字符串,并且在调用的时候插入其他的变量:
- 模板字符串被拆分了
- 第一个元素是数组,是被模块字符串拆分的字符串组合
- 后面的元素是一个个模块字符串传入的内容
jsfunction foo(...args) { console.log(args) } const nickName = 'later' const age = 18 foo`my name is ${nickName}, age is ${age}` // [['my name is ', ', age is ', ''], 'later', 18]
3. React的styled-components库
这就是标签模板字符串的一种应用场景:
六. ES6函数的增强用法
1. 函数的默认参数
在
ES6
之前,我们编写的函数参数是没有默认值的,所以我们在编写函数时,如果有下面的需求:- 传入了参数,那么使用传入的参数
- 没有传入参数,那么使用一个默认值
es5
实现jsfunction foo(arg1) { // 不严谨写法一 arg1 = arg1 ? arg1 : 'default' // 只要为假值,就会使用默认值 // 不严谨写法二 arg1 = arg1 || 'default' // 只要为假值,就会使用默认值 // 严谨写法一 arg1 = (arg1 !== undefined && arg1 !== null) ? arg1 : 'default' // 等于null或undefined,才会使用默认值 // 严谨写法二 // es6之后新增语法:?? 逻辑空赋值 arg1 = arg1 ?? 'default' // 等于null或undefined,才会使用默认值 console.log(arg1) } foo() foo(null) foo('') foo(0) foo(undefined)
es6
默认参数在
ES6
中,我们允许给函数的参数一个默认值:- 注意:经过
babel
转化后默认参数的es5
代码,发现只有实参为undefined
时,才会使用默认参数
jsfunction foo(arg1 = 'default') { console.log(arg1) } foo() // default foo(null) // null foo('') // '' foo(0) // 0 foo(undefined) // default // 注意: 经过babel转化后默认参数的es5代码,发现默认参数只会对undefined进行处理 function foo() { var arg1 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'default' }
- 注意:经过
2. 默认参数的补充
另外参数的默认值我们通常会将其放到最后(在很多语言中,如果不放到最后其实会报错的):
- 但是
JS
允许不将其放到最后,但是意味着还是会按照顺序来匹配
- 但是
另外默认值会改变函数的
length
的个数,默认值以及后面的参数都不计算在length
之内了js// 1. 注意一:有默认参数的形参尽量放到后面 function fn1(arg1 = 'default', arg2) { console.log(arg1, arg2) } fn1(1) // 1 undefined function fn2(arg1, arg2 = 'default') { console.log(arg1, arg2) } fn2(1) // 1 'default' // 2. 注意二:有默认值的形参是不会计算到函数的length属性之内(并且后面所有的参数都不会计算在length之内) function bar(arg1, arg2 = 'default', arg3) {} console.log(bar.length) // 1 // 3. 注意三:剩余参数必须放到最后 // function foo(age, name = 'default', ...args) { // console.log(age, name, args) // } function foo(age, name = 'default', ...args) { console.log(age, name, args) } foo(18, 'abc', 'cba', 'nba') // 18 'abc' (2) ['cba', 'nba']
默认值也可以和解构一起来使用:
jsconst obj = { name: 'later', age: 23 } function foo(obj = { name: 'later', age: 18 }) { console.log(obj.name, obj.age) } foo() // later 18 foo(obj) // later 23 function bar({ name, age } = { name: 'later', age: 18 }) { console.log(name, age) } bar() // later 18 bar(obj) // later 23 function baz({ name = 'later', age = 18 } = {}) { console.log(name, age) } baz() // later 18 baz(obj) // later 23
3. 函数的剩余参数
ES6
中引用了rest parameter
,可以将不定数量的参数放入到一个数组中:- 如果最后一个参数是
...
为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组
jsfunction foo(arg1, ...args) { console.log(args) } foo(1, 2, 3) // [2, 3]
- 如果最后一个参数是
那么剩余参数和
arguments
有什么区别呢?- 剩余参数只包含那些没有对应形参的实参,而
arguments
对象包含了传给函数的所有实参 arguments
对象不是一个真正的数组,是一个类数组对象,可以使用数组的索引和length
,而rest
参数是一个真正的数组,可以进行数组的所有操作arguments
是早期的ECMAScript
中为了方便去获取所有的参数提供的一个数据结构,而rest
参数是ES6
中提供并且希望以此来替代arguments
的
注意:
- 剩余参数必须放到最后一个位置,否则会报错
- 剩余参数只包含那些没有对应形参的实参,而
4. 箭头函数的补充
在前面我们已经学习了箭头函数的用法,这里进行一些补充:
- 箭头函数是没有显式原型
prototype
的,所以不能作为构造函数,使用new
来创建对象 - 箭头函数也不绑定
this
、arguments
、super
参数class
中的constructor
可以写成箭头函数,但是尽量避免那样去做,因为箭头函数不绑定super
、this
、arguments
这些
js// 1.function定义的函数是有两个原型的: function foo() {} console.log( foo.prototype ) // new foo() -> f.__proto__ = foo.prototype console.log( foo.__proto__ ) // -> Function.prototype // 2.箭头函数是没有显式原型 // 在ES6之后, 定义一个类要使用class定义 var bar = () => {} console.log( bar.__proto__ === Function.prototype ) // 没有显式原型 console.log( bar.prototype ) // undefined var b = new bar() // TypeError:bar is not a constructor
- 箭头函数是没有显式原型