新的ECMA代码执行描述
◼ 在执行学习JavaScript代码执行过程中,我们学习了很多ECMA文档的术语:
执行上下文栈:Execution Context Stack,用于执行上下文的栈结构;
执行上下文:Execution Context,代码在执行之前会先创建对应的执行上下文;
变量对象:Variable Object,上下文关联的VO对象,用于记录函数和变量声明;
全局对象:Global Object,全局执行上下文关联的VO对象;
激活对象:Activation Object,函数执行上下文关联的VO对象;
作用域链:scope chain,作用域链,用于关联指向上下文的变量查找;
◼ 在新的ECMA代码执行描述中(ES5以及之上),对于代码的执行流程描述改成了另外的一些词汇:
基本思路是相同的,只是对于一些词汇的描述发生了改变;
执行上下文栈和执行上下文也是相同的;
词法环境( Lexical Environments )
◼ 词法环境是一种规范类型,用于在词法嵌套结构中定义关联的变量、函数等标识符;
一个词法环境是由环境记录(Environment Record)和一个外部词法环境(
oute;r Lexical Environment)组成;
关联的一个函数声明,代码块,try-catch语句,当他们的代码被执行的时候,词法环境被创建出来的
一个词法环境经常用于关联一个函数声明、代码块语句、try-catch语句,当它们的代码被执行时,词法环境被创建出来;
◼ 也就是在ES5之后,执行一个代码,通常会关联对应的词法环境;
那么执行上下文会关联哪些词法环境呢?
LexicalEnvironment和VariableEnvironment
◼ LexicalEnvironment用于处理let、const声明的标识符:
◼ VariableEnvironment用于处理var和function声明的标识符:
环境记录(Environment Record)
◼ 在这个规范中有两种主要的环境记录值:声明式环境记录和对象环境记录。
声明式环境记录:声明性环境记录用于定义ECMAScript语言语法元素的效果,如函数声明、变量声明和直接将标识符绑定与
ECMAScript语言值关联起来的Catch子句。
对象式环境记录:对象环境记录用于定义ECMAScript元素的效果,例如WithStatement,它将标识符绑定与某些对象的属性
关联起来。
新ECMA描述内存图
let/const基本使用
◼ 在ES5中我们声明变量都是使用的var关键字,从ES6开始新增了两个关键字可以声明变量:let、const
let、const在其他编程语言中都是有的,所以也并不是新鲜的关键字;
但是let、const确确实实给JavaScript带来一些不一样的东西;
◼ let关键字:
从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个变量;
◼ const关键字:
const关键字是constant的单词的缩写,表示常量、衡量的意思;
它表示保存的数据一旦被赋值,就不能被修改;
但是如果赋值的是引用类型,那么可以通过引用找到对应的对象,修改对象的内容;
◼ 注意:
另外let、const不允许重复声明变量;
<script>
function foo() {
console.log("foo function")
}
var message = "Hello World"
</script>
let/const作用域提升
◼ let、const和var的另一个重要区别是作用域提升:
我们知道var声明的变量是会进行作用域提升的;
但是如果我们使用let声明的变量,在声明之前访问会报错;
◼ 那么是不是意味着foo变量只有在代码执行阶段才会创建的呢?
事实上并不是这样的,我们可以看一下ECMA262对let和const的描述;
这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值;
暂时性死区 (TDZ)
3
◼ 我们知道,在let、const定义的标识符真正执行到声明的代码之前,是不能被访问的
从块作用域的顶部一直到变量声明完成之前,这个变量处在暂时性死区(TDZ,temporal dead zone)
◼ 使用术语 “temporal” 是因为区域取决于执行顺序(时间),而不是编写代码的位置
<script>
// ES6之前
var message1 = "Hello World"
message1 = "Hello Coderwhy"
message1 = "aaaaa"
console.log(message1)
// ES6开始
// 1.let
let message2 = "你好, 世界"
message2 = "你好, why"
message2 = 123
console.log(message2)
// 2.const
// const message3 = "nihao, shijie"
// message3 = "nihao, why"
// 赋值引用类型
const info = {
name: "why",
age: 18
}
// info = {}
info.name = "kobe"
console.log(info)
</script>
let/const有没有作用域提升呢?
◼ 从上面我们可以看出,在 执行上下文的词法环境创建出来的时候 , 变量事实上已经被创建 了,只是 这个变量是不能被访问 的。
那么变量已经有了,但是不能被访问,是不是一种作用域的提升呢?
◼ 事实上维基百科并没有对作用域提升有严格的概念解释,那么我们自己从字面量上理解;
作用域提升: 在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升;
在这里,它虽然被创建出来了,但是不能被访问,我认为不能称之为作用域提升;
◼ 所以我的观点是let、const没有进行作用域提升,但是会在解析阶段被创建出来。
<script>
// 1.var变量可以重复声明
// var message = "Hello World"
// var message = "你好, 世界"
// 2.let/const不允许变量的重复声明
// var address = ""
let address = "广州市"
// let address = "上海市"
const info = {}
// const info = {}
</script>
早期语言上设计缺陷
Window对象添加属性
◼ 我们知道,在全局通过var来声明一个变量,事实上会在window上添加一个属性:
但是let、const是不会给window上添加任何属性的。
◼ 那么我们可能会想这个变量是保存在哪里呢?
<script>
// 1.var声明的变量会进行作用域的提升
// console.log(message)
// var message = "Hello World"
// 2.let/const声明的变量: 没有作用域提升
// console.log(address)
console.log(address)
let address = "广州市"
const info = {}
</script>
有被提前创建出来的,但是没有被方法问的,需要被词法版的滚过的
没有作用域提升
var的块级作用域
◼ 在我们前面的学习中,JavaScript只会形成两个作用域: 全局作用域和函数作用域
◼ ES5中放到一个代码中定义的变量,外面是可以访问的
//var没有块级作用域 通过var 声明的变量或者非严格模式下(non-strict mode)创建的函数声明没有块级作 1/编写语句 var foo - 用域。在语句块里声明的变量的作用域不仅是其所在的函数或者script,标签内,所设置 变量的影响会在超出语句块本身之外持续存在。换句话说,这种语句块不会引入一个作 用域。尽管单独的语句块是合法的语句,但在javascript中你不会想使用单独的语句 console.log(foo)//foo 可以访问到
let/const的块级作用域
◼ 在ES6中新增了块级作用域,并且通过 let、const、function、class声明 的标识符是具备块级作用域的限制的:
◼ 但是我们会发现 函数拥有块级作用域 ,但是 外面依然是可以访问 的:
这是因为引擎会对函数的声明进行特殊的处理,允许像var那样进行提升;
块级作用域的应用
◼ 我来看一个实际的案例:获取多个按钮监听点击
◼ 使用let或者const来实现:
<button>按钮0</button>
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
<script>
// 1.形成的词法环境
// var message = "Hello World"
// var age = 18
// function foo() {}
// let address = "广州市"
// {
// var height = 1.88
// let title = "教师"
// let info = "了解真相~"
// }
// 2.监听按钮的点击
const btnEls = document.querySelectorAll("button")
// [btn1, btn2, btn3, btn4]
// for (var i = 0; i < btnEls.length; i++) {
// var btnEl = btnEls[i];
// // btnEl.index = i
// (function(m) {
// btnEl.onclick = function() {
// debugger
// console.log(`点击了${m}按钮`)
// }
// })(i)
// }
for (let i = 0; i < btnEls.length; i++) {
const btnEl = btnEls[i];
btnEl.onclick = function() {
console.log(`点击了${i}按钮`)
}
}
// console.log(i)
</script>
var、let、const的选择
◼ 那么在开发中,我们到底应该选择使用哪一种方式来定义我们的变量呢?
◼ 对于var的使用:
我们需要明白一个事实,var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些历史遗
留问题;
其实是JavaScript在设计之初的一种语言缺陷;
当然目前市场上也在利用这种缺陷出一系列的面试题,来考察大家对JavaScript语言本身以及底层的理解;
但是在实际工作中,我们可以使用最新的规范来编写,也就是不再使用var来定义变量了;
◼ 对于let、const:
对于let和const来说,是目前开发中推荐使用的;
我们会优先推荐使用const,这样可以保证数据的安全性不会被随意的篡改;
只有当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用let;
这种在很多其他语言里面也都是一种约定俗成的规范,尽量我们也遵守这种规范;
// 1.var定义的变量是会默认添加到window上的
// var message = "Hello World"
// var address = "广州市"
// console.log(window.message)
// console.log(window.address)
// 2.let/const定义的变量不会添加到window上的
// let message = "Hello World"
// let address = "广州市"
// console.log(window.message)
// console.log(window.address)
// 3.let/var分别声明变量
var message = "Hello World"
let adress = "广州市"
function foo() {
debugger
}
foo()
let/const块级作用域
script>
// 1.在ES5以及之前, 只有全局和函数会形成自己的作用域
// 代码块
// function foo() {
// console.log("Hello World")
// }
// {
// var message = "Hello World"
// }
// console.log(message)
// 2.从ES6开始, 使用let/const/function/class声明的变量是有块级作用域
// console.log(message)
// foo()
{
var message = "Hello World"
let age = 18
const height = 1.88
class Person {}
function foo() {
console.log("foo function")
}
}
// console.log(age)
// console.log(height)
// const p = new Person()
foo()
</script>
let/const/function/class
加上立即执行函数形成自己的作用域,改成let
模板字符串
<script>
const name = "why"
const age = 18
// 1.基本用法
// 1.1.ES6之前
// const info = "my name is" + name + ", age is " + age
// 1.2.ES6之后
const info = `my name is ${name}, age is ${age}`
console.log(info)
// 2.标签模板字符串的用法
function foo(...args) {
console.log("参数:", args)
}
// foo("why", 18, 1.88)
foo`my name is ${name}, age is ${age}, height is ${1.88}`
</script>
字符串模板基本使用
◼ 在ES6之前,如果我们想要将字符串和一些动态的变量(标识符)拼接到一起,是非常麻烦和丑陋的(ugly) 。
◼ ES6允许我们使用字符串模板来嵌入JS的变量或者表达式来进行拼接:
首先,我们会使用 `` 符号来编写字符串,称之为模板字符串;
其次,在模板字符串中,我们可以通过 ${expression} 来嵌入动态的内容;
标签模板字符串使用
◼ 模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。 ◼ 我们一起来看一个普通的JavaScript的函数:
◼ 如果我们使用标签模板字符串,并且在调用的时候插入其他的变量:
模板字符串被拆分了;
第一个元素是数组,是被模块字符串拆分的字符串组合;
后面的元素是一个个模块字符串传入的内容;
React的styled-components库
函数的默认参数
◼ 在ES6之前,我们编写的函数参数是没有默认值的,所以我们在编写函数时,如果有下面的需求:
传入了参数,那么使用传入的参数;
没有传入参数,那么使用一个默认值;
◼ 而在ES6中,我们允许给函数一个默认值:
<script>
// 注意: 默认参数是不会对null进行处理的
function foo(arg1 = "我是默认值", arg2 = "我也是默认值") {
// 1.两种写法不严谨
// 默认值写法一:
// arg1 = arg1 ? arg1: "我是默认值"
// 默认值写法二:
// arg1 = arg1 || "我是默认值"
// 2.严谨的写法
// 三元运算符
// arg1 = (arg1 === undefined || arg1 === null) ? "我是默认值": arg1
// ES6之后新增语法: ??
// arg1 = arg1 ?? "我是默认值"
// 3.简便的写法: 默认参数
console.log(arg1)
}
foo(123, 321)
foo()
foo(0)
foo("")
foo(false)
foo(null)
foo(undefined)
</script>
默认参数注意
<script>
// 1.注意一: 有默认参数的形参尽量写到后面
// 2.有默认参数的形参, 是不会计算在length之内(并且后面所有的参数都不会计算在length之内)
// 3.剩余参数也是放到后面(默认参数放到剩余参数的前面)
function foo(age, name = "why", ...args) {
console.log(name, age, args)
}
foo(18, "abc", "cba", "nba")
console.log(foo.length)
</script>
剩余参数也是放到后面(默认参数放到剩余参数的前面)
有默认参数的形参, 是不会计算在length之内(并且后面所有的参数都不会计算在length之内)`