《React+Redux前端开发实战》—1.1.7 ES 6语法
1.1.7 ES 6语法
ES 6(ECMAScript 6.0)是一个历史名词,也是一个泛指,指代ECMAScript 5.1版本之后JavaScript的下一代标准。其中包含ES 2015、ES 2016和ES 2017等,而这些年份表示在当年发布的正式版本的语言标准。
最早的ECMAScript 1.0于1997年发布,ECMAScript 2.0于1998年发布,ECMAScript 3.0于1999年发布。有意思的是,2000年ECMAScript 4.0的草案由于太过于激进没有被发布。到了2007年,ECMAScript 4.0草案发布。当时以Microsoft和Google为首的互联网“巨头”反对ES的大幅升级,希望能小幅改动;而Brendan Eich(JavaScript创造者)为首的Mozilla公司坚持当时的草案。由于分歧太大,2018年而中止了对ECMAScript 4.0的开发,将其中激进的部分放到以后的版本,将其中改动小的部分发布为ECMAScript 3.1,之后又将其改名为ECMAScript 5,并于2009年12月发布。在2015年6月,ECMAScript 6正式通过。但很多人不知道,时至今日,JavaScript初学者学习的其实就是ES 3.0版本。目前为止,各大浏览器厂商对ES 6语法特性的支持已经超过90%。
以上是对ECMAScript语言规范的简单历史介绍。
由于本书使用的示例代码会涉及ES 6相关语法,因此下面对项目中经常使用到的几点特性进行简单介绍。
1.变量声明let和const
ES 6之前,通常用var关键字来声明变量。无论在何处声明,都会被视为在所在函数作用域最顶部(变量提升)。那么为什么需要用let和const关键词来创建变量或常量呢?理由是:
可以解决ES 5使用var初始化变量时会出现的变量提升问题;
可以解决使用闭包时出错的问题;
ES 5只有全局作用域和函数作用域,却没有块级作用域;
可以解决使用计数的for循环变量时会导致泄露为全局变量的问题。
let命令表示被声明的变量值在作用域内生效。比如:
{
let a = 1;
var b = 2;
}
a // 报错ReferenceError
b // 2
提示:以上代码可以在浏览器开发工具的Console模式中调试。
从上述代码可以看出,let声明的代码只能在其所在的代码块内有效,出了代码块,就会出错。
另外,对于let来说,不存在变量提升。在一般代码逻辑中,变量应该是定义后才可以使用,但var的变量提升却可以先使用再定义,多少有些“奇怪”,而let就更改了这种语法行为。要使用一个变量必须先声明,不然就报错,显然这样更合理。
var和let的对比示例:
Console.log(a); // undefined
var a = 1;
Console.log(a) // 报错 ReferenceError
let a = 1;
此外,let不允许重复声明,比如:
// 报错
function func(){
let a = 1;
var a = 2;
}
// 报错
function func(){
let a = 1;
let a = 2;
}
在代码块内,使用let声明变量之前,该变量都是不可用的(不可访问、不可赋值等)。在语法上,这被称为“暂时性死区”(Temporal Dead Zone,TDZ)。
注意:暂时性死区就是只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取。只有等到声明变量的那一行代码出现,才可以获取和使用该变量。例如:
if (true) {
// TDZ开始,不可访问,不可赋值
temp = "hello"; // ReferenceError
console.log(temp); // ReferenceError
let temp; // TDZ结束
console.log(temp); // 输出undefined,可访问
temp = 1; // 可赋值
console.log(temp); // 输出1,访问
}
在ES 5中,变量提升可能还会导致内层变量覆盖外层变量,比如:
var i = 1;
function func() {
console.log(i);
if (true) {
var i = 1;
}
}
func(); // undefined
let还引入了块级作用域的概念,传统ES 5中不存在块级作用域。假如没有块级作用域,可能会碰到这种问题:
var arr = [1, 2, 3, 4]
for (var i = 0; i < arr.length; i++){
console.log(i);
}
console.log(i); // 4
上述代码希望达到的结果是,在for循环之后变量i被垃圾回收机制回收。但用来计数的循环变量在循环结束后并没有被回收,内存泄露为了全局变量。这种场景非常适合使用块级作用域let:
var arr = [1, 2, 3, 4]
for (let i = 0; i < arr.length; i++){
console.log(i);
}
console.log(i); // Uncaught ReferenceError: i is not defined
从上面的示例代码可以看出,当循环结束后,就可以将不需要的用于计数的变量回收,让它消失。虽然一个简单的变量泄漏并不会造成很大危害,但这种写法是错误的。
块级作用域的出现无疑带来了很多好处,它允许作用域的任意嵌套,例如:
{{{{
{let i = 1;}
console.log(i); // 报错
}}}}
内层作用域可以使用跟外层同名的变量名,比如:
{{{{
let i =1;
console.log(i); // 1
{
let i = 2;
console.log(i); // 2
}
}}}}
块级作用域还使立即执行函数表达式(IIFE)不再成为必要项,比如:
// 立即执行函数
(function () {
var a = ...;
...
}());
// 块级作用域写法
{
let a = ...;
...
}
再来看看const。const用于声明只读的常量,一旦声明就不能改变。和let一样,const只在块级作用域内有效,不存在变量提升,存在暂时性死区和不可重复声明。
2.解构赋值
按照一定模式从数组或对象中提取值,对变量进行赋值,叫做解构赋值(Destructuring)。
注意:解构赋值的对象是数组或对象,作用是赋值。
用于对象的解构赋值示例:
const cat = {
name: 'Tom',
sex: 'male',
age: 3
};
let { name, sex, age } = cat;
console.log(name, sex, age); // Tom male 3
上述代码将cat中的属性解构出来并赋值给name、sex和age。同样的示例,传统写法如下:
const cat = {
name: 'Tom',
sex: 'male',
age: 3
};
let name = cat.name;
let sex = cat.sex;
let age = cat.age;
对象解构也可以指定默认值:
var {a =1} = {};
a // 1
var {a, b = 2} = {a: 1}
a // 1
b // 2
当解构不成功时,变量的值为undefined:
let {a} = {b: 1};
a // undefined
ES 6的解构赋值给开发者带来了很大的便捷,这就是解构赋值的魅力。同样,解构赋值也能在数组中使用。
数组的解构赋值示例:
let [a, b , c] = [1, 2, 3];
a // 1
b // 2
c // 3
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [e, f, …g] = ["hello"];
e // "hello"
f // undefined
g // [ ]
以上代码表明可以从数组中提取值,按照对应位置赋值给对应变量。如果解构失败就会赋值为undfined 。如果等号右边是不可遍历的结构,也会报错。
// 报错
let [a] = 1;
let [a] = false;
let [a] = {};
let [a] = NaN;
let [a] = undefined;
以上都会报错。
在解构赋值中也允许有默认值,例如:
let {a = [1, 2, 3]} = { };
a // [1, 2, 3]
let [x, y = 'hi'] = ["a"];
x // x='a', y='b'
3.拓展运算符(spread)…
拓展运算符(spread)是3个点(…)。可以将它比作rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。下面来看看它有哪些作用。
(1)合并数组。
在ES 5中要合并两个数组,写法是这样的:
var a = [1, 2];
var b = [3, 4];
a.concat(b); // [1, 2, 3, 4]
但在ES 6中拓展运算符提供了合并数组的新写法:
let a = [1, 2];
let b = [3, 4];
[...a, …b]; // [1, 2, 3, 4]
如果想让一个数组添加到另一个数组后面,在ES 5中是这样写的:
var x = ["a", "b"];
var y = ["c", "d"];
Array.prototype.push.apply(arr1, arr2);
arr1 // ["a", "b", "c", "d"]
上述代码中由于push()方法不能为数组,所以通过apply()方法变相使用。但现在有了ES 6的拓展运算符后就可以直接使用push()方法了:
let x = ["a", "b"];
let y = ["c", "d"];
x.push(…y); // ["a", "b", "c", "d"]
(2)数组复制。
拓展运算符还可以用于数组复制,但要注意的是,复制的是指向底层数据结构的指针,并非复制一个全新的数组。
数组复制示例:
const x = ['a', 'b'];
const x1 = x;
x1[0]; // 'a'
x1[0] = 'c';
x // ['c', 'b']
(3)与解构赋值结合。
拓展运算符可以和解构赋值相结合用于生成新数组:
const [arr1, …arr2] = [1, 2, 3, 4];
arr1 // 1
arr2 // [2, 3, 4]
注意,使用拓展运算符给数组赋值时,必须放在参数最后的位置,不然会报错。例如:
const […arr1, arr2] = [1, 2, 3, 4]; // 报错
const [1, …arr1, arr2] = [1, 2, 3, 4]; // 报错
(4)函数调用(替代apply()方法)。
在ES 5中要合并两个数组,写法是这样的:
function add(a, b) {
return a + b;
}
const num = [1, 10];
add.apply(null, num); // 11
在ES 6中可以这样写:
function add(a, b) {
return a + b;
}
const num = [1, 10];
add(…num); // 11
上述代码使用拓展运算符将一个数组变为参数序列。当然,拓展运算符也可以和普通函数参数相结合使用,非常灵活。比如:
function add(a, b, c, d) {
return a + b + c + d;
}
const num = [1, 10];
add(2, …num, -2); // 11
拓展运算符中的表达式如下:
[…(true [1, 2] : [3]), 'a']; // [1, 2, 'a']
4.箭头函数
ES 6对于函数的拓展中增加了箭头函数=>,用于对函数的定义。
箭头函数语法很简单,先定义自变量,然后是箭头和函数主体。箭头函数相当于匿名函数并简化了函数定义。
不引入参数的箭头函数示例:
var sum = () => 1+2; // 圆括号代表参数部分
// 等同于
var sum = function() {
return 1 + 2;
}
引入参数的箭头函数示例:
// 单个参数
var sum = value => value; // 可以不给参数value加小括号
// 等同于
var sum = function(value) {
return value;
};
// 多个参数
var sum = (a, b) => a + b;
// 等同于
var sum = function(a, b) {
return a + b;
};
花括号{}内的函数主体部分写法基本等同于传统函数写法。
注意:如果箭头函数内要返回自定义对象,需要用小括号把对象括起来。例如:
var getInfo = id =>({
id: id,
title: 'Awesome React'
});
// 等同于
var getInfo = function(id) {
return {
id: id,
title: 'Awesome React'
}
}
箭头函数与传统的JavaScript函数主要区别如下:
箭头函数内置this不可改变;
箭头函数不能使用new关键字来实例化对象;
箭头函数没有arguments对象,无法通过arguments对象访问传入的参数。
这些差异的存在是有理可循的。首先,对this的绑定是JavaScript错误的常见来源之一,容易丢失函数内置数值,或得出意外结果;其次,将箭头函数限制为使用固定this引用,有利于JavaScript引擎优化处理。
箭头函数看似匿名函数的简写,但与匿名函数有明显区别,箭头函数内部的this是词法作用域,由上下文确定。如果使用了箭头函数,就不能对this进行修改,所以用call()或apply()调用箭头函数时都无法对this进行绑定,传入的第1个参数会被忽略。
更多详情,可参考阮一峰的《ECMAScript 6入门》一书。
注意:词法作用域是定义在词法阶段的作用域,它在代码书写的时候就已经确定。
- 点赞
- 收藏
- 关注作者
评论(0)