Loading... ## ECMAScript 6简介 ### 概述 ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。 ### ECMAScript 和 JavaScript 的关系 要讲清楚这个问题,需要回顾历史。1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。 该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性和中立性。 因此,ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript)。日常场合,这两个词是可以互换的。 ### ES6 与 ECMAScript 2015 的关系 2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。 但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包括所有将要引入的功能。常规的做法是先发布 6.0 版,过一段时间再发 6.1 版,然后是 6.2 版、6.3 版等等。 但是,标准的制定者不想这样做。他们想让标准的升级成为常规流程:任何人在任何时候,都可以向标准委员会提交新语法的提案,然后标准委员会每个月开一次会,评估这些提案是否可以接受,需要哪些改进。如果经过多次会议以后,一个提案足够成熟了,就可以正式进入标准了。这就是说,标准的版本升级成为了一个不断滚动的流程,每个月都会有变动。 标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。 ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的 `includes`方法和指数运算符),基本上是同一个标准。根据计划,2017 年 6 月发布 ES2017 标准。 因此,ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。 ## let和const命令 ### let命令 **概述:** ES6 新增了 `let`命令,用来声明变量。它的用法类似于 `var`,但是所声明的变量,只在 `let`命令所在的代码块内有效。 **语法:**`let 变量名 = 值;` <div class="tip inlineBlock error simple"> **⚠️注意:** 在使用let命令声明变量时包含如下特性: * let命令作用域只局限于当前代码块 ```javascript // 创建一个作用域 { var a = 10; let b = 1; } console.log(a); //输出:10 console.log(b); //Uncaught ReferenceError: b is not defined ``` * let命令声明的变量作用域不会被提前(不会被预解析),**暂时性死区** ```javascript console.log(a); //undefined var a = 10; console.log(b); //Uncaught ReferenceError: Cannot access 'b' before initialization (初始化前无法访问“b”) let b = 20; var tmp = 123; if (true) { tmp = 'abc'; // Uncaught ReferenceError: Cannot access 'tmp' before initialization (暂时性死区) let tmp; } ``` * 在相同的作用域下,不允许同时声明相同的let变量 ```javascript var a = 10; var a = 20; console.log(a); let b = 10; let b = 20; // Uncaught SyntaxError: Identifier 'b' has already been declared (标识符“b”已声明) console.log(b); ``` </div> **块级作用域:** ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。 第一种场景,内层变量可能会覆盖外层变量。 ```javascript var tmp = new Date(); function f() { console.log(tmp); if (false) { var tmp = 'hello world'; } } f(); // undefined ``` ⭐️ 上面代码中, `if`代码块的外部使用外层的 `tmp`变量,内部使用内层的 `tmp`变量。但是,函数 `f`执行后,输出结果为 `undefined`,原因在于变量提升,导致内层的 `tmp`变量覆盖了外层的 `tmp`变量。 第二种场景,用来计数的循环变量泄露为全局变量。 ```javascript var s = 'hello'; for (var i = 0; i < s.length; i++) { console.log(s[i]); } console.log(i); // 5 ``` ⭐️上面代码中,变量 `i`只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。 在ES6中新增的块级作用域: ```javascript function show1() { let a = 10; if (true) { let a = 20; } console.log(a); } function show2() { var a = 10; if (true) { var a = 20; } console.log(a); } //调用方法,查看输出结果 show1(); show2(); /** * 输出结果为: * 10 * 20 */ ``` ⭐️上面的函数有两个代码块,都声明了变量 `a`,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用 `var`定义变量 `a`,最后输出的值才是 10。 在ES6中,允许块级作用域的任意嵌套。 ### const命令 **概述:** `const`声明一个只读的常量。一旦声明,常量的值就不能改变。 <div class="tip inlineBlock error simple"> **⚠️注意:** * const在声明时,必须赋值。 * 只在声明所在的块级作用域内有效。 * const命令声明的常量不提升。 * 存在暂时性死区,只能在声明的位置后面使用 * 声明的常量,也与let一样不可重复声明 </div> **案例演示:** ```javascript // 定义常量 const PI = 3.14; // 修改常量值 PI = 10; // Assignment to constant variable (给常量赋值) // 定义常量未赋值 const Week; // Missing initializer in const declaration(常量声明中缺少初始值设定项) ``` **查看下方代码,并说出运行结果:** ```javascript function f1() { const m = 10; if (true) { let n = 10; m = 20; } console.log(n); } f1(); ``` ## 解构赋值 ### 数组解构赋值 **概述:**ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。 以前,为变量赋值,只能直接指定值: ```javascript let a = 1; let b = 2; let c = 3; ``` ES6 允许写成下面这样: ```javascript let [a, b, c] = [1, 2, 3]; ``` 上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。 本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。 **案例演示:** ```javascript // 可以嵌套 let [foo, [[bar], baz]] = [1, [[2], 3]]; foo // 1 bar // 2 baz // 3 // 可以忽略 let [ , , third] = ["foo", "bar", "baz"]; third // "baz" // 不完全解构 let [x, y] = [1, 2, 3]; x // 1 y // 2 // 剩余运算符 let [head, ...tail] = [1, 2, 3, 4]; head // 1 tail // [2, 3, 4] // 解构默认值 let [x, y = 'b'] = ['a']; console.log(x); //'a' console.log(y); //'b' // 如果解构不成功,变量的值就等于undefined let [x, y, ...z] = ['a']; x // "a" y // undefined z // [] ``` **扩展案例-使用结构实现两数交换:** ```javascript let [x, y] = [1, 2]; console.log(`x:${x},y:${y}`); //x:1,y:2 //开始交换 [x, y] = [y, x]; console.log(`x:${x},y:${y}`); //x:2,y:1 ``` ### 对象解构赋值 **概述:**解构不仅可以用于数组,还可以用于对象。对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。 ```javascript let { bar, foo } = { foo: 'aaa', bar: 'bbb' }; foo // "aaa" bar // "bbb" let { baz } = { foo: 'aaa', bar: 'bbb' }; baz // undefined ``` 上面代码中,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。而第二个例子中的变量没有对应的同名属性,导致取不到值,最后等于 `undefined`。 ⭐️如果解构失败,变量的值等于 `undefined`。 **案例演示:** ```javascript //可嵌套 let obj = { p: ['hello', { y: 'world' }] }; let { p: [x, { y }] } = obj; console.log(x); //hello console.log(y); //world //可忽略 let obj = { p: ['hello', { y: 'world' }, 'ECMAScript'] }; let { p: [x, { y }] } = obj; console.log(x); //hello console.log(y); //world //不完全解构 let obj = { p: [{ y: 'world' }, 'hello'] }; let { p: [{ y }, x, z] } = obj; console.log(x, y, z); //hello ,world,undefined //剩余运算符 let { a, b, ...rest } = { a: 10, b: 20, c: 30, d: 40 }; console.log(a, b); //10,20 console.log(rest); //{c: 30, d: 40} //解构默认值 let { a = 10, b = 5 } = { a: 3 }; console.log(a); //3 console.log(b); //5 ``` **分析下面的代码,它的作用是什么:** ```javascript let { log } = console; log("msg"); ``` **分析下面的代码,输出结果是什么:** ```javascript let obj = {p: ['hello', {y: {z: 'world'}}, 'ECMAScript'] }; let {p: [x, { y }] } = obj; console.log(x); console.log(y); ``` ### 字符串解构赋值 **概述:**字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。 数组的对象都有一个 `length`属性,因此还可以对这个属性解构赋值。 ```javascript const [a, b, c, d, e] = 'hello'; console.log(a) // "h" console.log(b) // "e" console.log(c) // "l" console.log(d) // "l" console.log(e) // "o" let { length: l } = 'hello' console.log(l); //5 ``` ### 函数参数解构赋值 **概述:** 除了数组、对象、字符串等可以使用解构赋值外,函数的参数也可以使用解构赋值。 ```javascript function add([x, y]){ return x + y; } let x = add([1, 2]); console.log(x); //3 ``` 上面代码中,函数 `add`的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量 `x`和 `y`。对于函数内部的代码来说,它们能感受到的参数就是 `x`和 `y`。 ### 变量解构的用途 1. 变量值的交换 2. 从函数返回多个值 3. 函数参数的定义 4. 提取 JSON 数据 5. 函数参数的默认值 6. 遍历 Map 结构 7. 导入模块的指定方法 【更多请访问:[ECMAScript 6 入门-变量的解构赋值-用途](https://es6.ruanyifeng.com/?search=%E5%9D%97%E7%BA%A7%E4%BD%9C%E7%94%A8%E5%9F%9F&x=0&y=0#docs/destructuring#%E7%94%A8%E9%80%94)】 ## 函数扩展 ### 函数参数默认值 **概述:** ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。 ```javascript function log(x, y) { y = y || 'World'; console.log(x, y); } log('Hello') // Hello World log('Hello', 'China') // Hello China log('Hello', '') // Hello World ``` 上面代码检查函数 `log()`的参数 `y`有没有赋值,如果没有,则指定默认值为 `World`。这种写法的缺点在于,如果参数 `y`赋值了,但是对应的布尔值为 `false`,则该赋值不起作用。就像上面代码的最后一行,参数 `y`等于空字符,结果被改为默认值。 **ES6的新写法:** ```javascript function log(x, y = 'World') { console.log(x, y); } log('Hello') // Hello World log('Hello', 'China') // Hello China log('Hello', '') // Hello ``` 在上面的代码中,ES6 的写法比 ES5 简洁许多,而且非常自然。 <div class="tip inlineBlock error simple"> **⚠️注意:** 参数变量是默认声明的,所以不能用 `let`或 `const`再次声明。 ```javascript function foo(x = 5) { let x = 1; // error const x = 2; // error } ``` 使用参数默认值时,函数不能有同名参数。 ```javascript // 不报错 function foo(x, x, y) { // ... } // 报错 function foo(x, x, y = 1) { // ... } // SyntaxError: Duplicate parameter name not allowed in this context ``` 参数默认值一定要放在普通参数之后。【该标准已被更改】 ```javascript function foo1(x, y = 10) { console.log(x, y) } function foo2(x = 10, y) { //在之前的标准,该函数的定义将会报错,在新标准中,则不会 console.log(x, y) } foo1(1); //1,10 foo2(1); //1,undefined ``` </div> #### 解构赋值与默认值结合使用 **演示案例:** ```javascript function foo({x, y = 5}) { console.log(x, y); } foo({}) // undefined 5 foo({x: 1}) // 1 5 foo({x: 1, y: 2}) // 1 2 foo() // TypeError: Cannot read property 'x' of undefined ``` 上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数 `foo()`的参数是一个对象时,变量 `x`和 `y`才会通过解构赋值生成。如果函数 `foo()`调用时没提供参数,变量 `x`和 `y`就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。 ```javascript function foo({x, y = 5} = {}) { console.log(x, y); } foo() // undefined 5 ``` #### 函数的 length 属性 指定了默认值以后,函数的 `length`属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,`length`属性将失效。 ```javascript (function (a) {}).length // 1 (function (a = 5) {}).length // 0 (function (a, b, c = 5) {}).length // 2 ``` 上面代码中,`length`属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了 3 个参数,其中有一个参数 `c`指定了默认值,因此 `length`属性等于 `3`减去 `1`,最后得到 `2`。 这是因为 `length`属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入 `length`属性。 如果设置了默认值的参数不是尾参数,那么 `length`属性也不再计入后面的参数了。 ```javascript (function (a = 0, b, c) {}).length // 0 (function (a, b = 1, c) {}).length // 1 ``` #### 作用域 一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。 ```javascript var x = 1; function f(x, y = x) { console.log(y); } f(2) // 2 ``` 上面代码中,参数 `y`的默认值等于变量 `x`。调用函数 `f`时,参数形成一个单独的作用域。在这个作用域里面,默认值变量 `x`指向第一个参数 `x`,而不是全局变量 `x`,所以输出是 `2`。 ### rest参数和name属性 **rest参数概述:** ES6 引入 rest 参数(形式为 `...变量名`),用于获取函数的多余参数,这样就不需要使用 `arguments`对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。 ```javascript function add(...values) { let sum = 0; for (var val of values) { sum += val; } return sum; } let res = add(2, 5, 3) console.log(res); // 10 ``` 上面代码的 `add`函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。 <div class="tip inlineBlock error simple"> **⚠️注意:**rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。 </div> **name属性概述:**函数的 `name`属性,返回该函数的函数名。 ```javascript function foo() { } console.log(foo.name); // "foo" ``` 这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。 需要注意的是,ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5 的 `name`属性,会返回空字符串,而 ES6 的 `name`属性会返回实际的函数名。 ```javascript var f = function () {}; // ES5 f.name // "" // ES6 f.name // "f" ``` 上面代码中,变量 `f`等于一个匿名函数,ES5 和 ES6 的 `name`属性返回的值不一样。 如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的 `name`属性都返回这个具名函数原本的名字。 ```javascript const bar = function baz() {}; // ES5 bar.name // "baz" // ES6 bar.name // "baz" ``` ### 箭头函数 在ES6 允许使用“箭头”(`=>`)定义函数。 ```javascript // 箭头函数定义 var f = v => v; // 等同于 var f = function (v) { return v; }; ``` 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。 ```javascript var f = () => 5; // 等同于 var f = function () { return 5 }; var sum = (num1, num2) => num1 + num2; // 等同于 var sum = function(num1, num2) { return num1 + num2; }; ``` 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用 `return`语句返回。 ```javascript var calc = (num1, num2, opt) => { let result = 0; switch (opt) { case "+": result = num1 + num2; break; case "-": result = num1 - num2; break; //... } return result; } ``` <div class="tip inlineBlock error simple"> **⚠️箭头函数有几个使用注意点。** (1)箭头函数没有自己的 `this`对象。⭐️ (2)不可以当作构造函数,也就是说,不可以对箭头函数使用 `new`命令,否则会抛出一个错误。 (3)不可以使用 `arguments`对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。 (4)不可以使用 `yield`命令,因此箭头函数不能用作 Generator 函数。 上面四点中,最重要的是第一点。对于普通函数来说,内部的 `this`指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的 `this`对象,内部的 `this`就是定义时上层作用域中的 `this`。也就是说,箭头函数内部的 `this`指向是固定的,相比之下,普通函数的 `this`指向是可变的。 </div> **案例分析:** ```javascript function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++; }, 1000); } var timer = new Timer(); setTimeout(() => console.log('s1: ', timer.s1), 3100); setTimeout(() => console.log('s2: ', timer.s2), 3100); // s1: 3 // s2: 0 ``` 上面代码中,`Timer`函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的 `this`绑定定义时所在的作用域(即 `Timer`函数),后者的 `this`指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,`timer.s1`被更新了 3 次,而 `timer.s2`一次都没更新。 **注意:**箭头函数的this不是调用的时候决定的,而是在定义的时候处在的对象就是它的this #### 箭头函数不适用于下列场景 第一个场合是定义对象的方法,且该方法内部包括 `this`。 ```javascript const cat = { lives: 9, jumps: () => { this.lives--; } } ``` 上面代码中,`cat.jumps()`方法是一个箭头函数,这是错误的。调用 `cat.jumps()`时,如果是普通函数,该方法内部的 `this`指向 `cat`;如果写成上面那样的箭头函数,使得 `this`指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致 `jumps`箭头函数定义时的作用域就是全局作用域。 ```javascript var s = 21; const obj = { s: 42, m: () => console.log(this.s) }; obj.m() // 21 ``` 上面例子中,`obj.m()`使用箭头函数定义。JavaScript 引擎的处理方法是,先在全局空间生成这个箭头函数,然后赋值给 `obj.m`,这导致箭头函数内部的 `this`指向全局对象,所以 `obj.m()`输出的是全局空间的 `21`,而不是对象内部的 `42`。上面的代码实际上等同于下面的代码。 ```javascript var s = 21; var m = () => console.log(this.s); const obj = { s: 42, m: m }; obj.m() // 21 ``` 第二个场合是需要动态this的时候,也不应使用箭头函数: ```javascript let box1 = document.getElementById('content-1'); let box2 = document.getElementById('content-2'); box1.onclick = () => { this.style.border = "2px solid red"; //执行错误,this: Window } box2.onclick = function () { this.style.border = "2px solid red"; //执行成功,this: div#content-2 } ``` 第三个场合是不能作为构造函数。 第四个场合是不能用在原型方法绑定: ```javascript function Person() { this.userName = "张三丰"; } Person.prototype.sayHi = () => { console.log("大家好,我叫:" + this.userName); } let stu = new Person(); stu.sayHi(); // 大家好,我叫:undefined ``` --- <div class="tip inlineBlock info simple"> ▶️ 开始学习下一章: [JavaScript+Jquery+ES6入门到放弃之ECMAScript 6 进阶](https://jbea.cn/archives/567.html) </div> 最后修改:2022 年 09 月 06 日 © 允许规范转载 赞 2 都滑到这里了,不点赞再走!?