本章是第七章ES6相关的内容,也是最后一章。本篇为第一部分,后面还会再有一篇。
现在ES6使用非常广泛,新增的箭头函数、类、Promise等新特性,可以方便地处理很多复杂的操作,极大地提高了开发效率。本章会记录ES6中最常用的新特性
在学完后,希望掌握下面知识点:
比起之前只有全局作用域和函数作用域,ES6中新增了块级作用域。
块级作用域表示的是定义的变量可执行上下文环境只能在一个代码块中,一个代码块由一个大括号括住的代码构成,超出这个代码块范围将无法访问内部的变量。
ES6中新增的let关键字和const关键字就是为块级作用域服务的。
let关键字用于声明变量,和var关键字的用法类似。但是与var不同的是,let声明的变量只能在let所在的代码块内有效,即在块级作用域内有效,而var声明的变量在块级作用域外仍然有效
{
var a = 1;
let b = 2;
}
console.log(a); // 1
console.log(b); // ReferenceError: b is not defined
// var声明的变量和函数表达式
var a = 1;
var fn = function () {
console.log('global method');
};
console.log(window.a); // 1
window.fn(); // global method
// let声明的变量和函数表达式
let b = 2;
let foo = function () {
console.log('global method');
};
console.log(window.b); // undefined
window.foo(); // TypeError: window.foo is not a function
// IIFE写法
(function(){
var arg = ...;
...
}());
// 块级作用域写法
{
let arg = ...;
...;
}
使用const声明的值为一个常量,一旦声明将不会再改变。
如果改变了const声明的常量,则会抛出异常。
const MAX = 123;
MAX = 456; // TypeError: Assignment to constant variable.
使用const声明常量时,在声明时就必须初始化。如果只声明,不初始化,则会抛出异常。
const MAX = 123; // 声明正常
const MIN; // SyntaxError: Missing initializer in const declaration
和let关键字拥有一样的特性
在日常开发中,我们经常会定义很多的数组或者对象,然后从数组或对象中提取出相关的信息。在传统的ES5及以前的版本中,我们通常会采用下面的方法获取数组或者对象中的值:
var arr = ['one', 'two', 'three'];
var one = arr[0];
var two = arr[1];
var obj = {name:'kingx', age:21};
var name = obj.name;
var age = obj.age;
如果数组选项或者对象属性值很多,那么在提取对应值的时候会写很多冗余的代码。
ES6增加了可以简化这种操作的新特性——解构,它可以将值从数组或者对象中提取出来,并赋值到不同的变量中。
主要分两方面:数组的解构赋值和对象的解构赋值
针对数组,在解构赋值时,使用的是模式匹配,只要等号两边数组的模式相同,右边数组的值就会相应赋给左边数组的变量
let [arg1, arg2] = [12, 34];
console.log(arg1); // 12
console.log(arg2); // 34
还可以只解构出感兴趣的值,对不感兴趣的值使用逗号作为占位符,而不指定变量名
let [,,num3] = [12, 34, 56];
console.log(num3); // 56
当右边的数组的值不足以将左边数组的值全部赋值时,会解构失败,对应的值就等于undefined
let [num1, num2, num3] = [12, 34];
console.log(num2); // 34
console.log(num3); // undefined
下面是可以提高效率的一些数组的解构赋值的应用场景:
在数组解构时设置默认值,可以防止出现解构得到undefined值的情况
let [num1 = 1, num2] = [, 34];
console.log(num1); // 1
console.log(num2); // 34
需要注意的是,ES6在判断解构是否会得到undefined值时,使用的是严格等于(===)。只有在严格等于undefined的情况下,才会判断该值解构为undefined,相应变量的默认值才会生效。注意下面例子中的num3
let [
num1 = 1,
num2 = 2,
num3 = 3
] = [null, ''];
console.log(num1); // null
console.log(num2); // ''
console.log(num3); // 3
使用数组解构赋值就可以不额外使用临时变量,只需要在等式两边的数组中交换两个变量的顺序即可
var a = 1;
var b = 2;
// 使用数组的解构赋值交换变量
[b, a] = [a, b];
console.log(a); // 2
console.log(b); // 1
函数返回数组是很常见的场景,在获取数组后我们经常会提取数组中的元素进行后续处理。
使用数组的解构赋值,我们可以快速地获取数组元素值:
function fn() {
return [12, 34];
}
let [num1, num2] = fn();
console.log(num1); // 12
console.log(num2); // 34
在遇到嵌套数组时,即数组中的元素仍然是一个数组,解构的过程会一层层深入,直到左侧数组中的各个变量均已得到确定的值
let [num1, num2, [num3]] = [12, [34, 56], [78, 89]];
console.log(num1); // 12
console.log(num2); // [34, 56]
console.log(num3); // 78
在上面的实例中,num2对应的位置是一个数组,得到的是“[34, 56]”;[num3]得到的是一个数组“[78, 89]”,解构并未完成,对于num3会继续进行解构,最后得到的是数组第一个值“78”
当函数的参数为数组类型时,可以将实参和形参进行解构
function foo([arg1, arg2]) {
console.log(arg1); // 2
console.log(arg2); // 3
}
foo([2, 3]);
数组的解构赋值是基于数组元素的索引,只要左右两侧的数组元素索引相同,便可以进行解构赋值。
但是在对象中,属性是没有顺序的,这就要求右侧解构对象的属性名和左侧定义对象的变量名必须相同,这样才可以进行解构。
同样,未匹配到的变量名在解构时会赋值“undefined”
let {m, n, o} = {m: 'kingx', n: 12};
console.log(m); // kingx
console.log(n); // 12
console.log(o); // undefined
当解构对象的属性名和定义的变量名不同时,必须严格按照key: value的形式补充左侧对象
let {m: name, n: age} = {m: 'kingx', n: 12}; console.log(name); // kingx
console.log(age); // 12
而当key和value值相同时,对于value的省略实际上是一种简写方案
let {m: m, n: n} = {m: 'kingx', n: 12};
// 简写方案
let {m, n} = {m: 'kingx', n: 12};
事实上,对象解构赋值的原理是:先找到左右两侧相同的属性名(key),然后再赋给对应的变量(value),真正被赋值的是value部分,并不是key的部分。
在如下所示的代码中,m作为key,只是用于匹配两侧的属性名是否相同,而真正被赋值的是右侧的name变量,最终name变量会被赋值为“kingx”,而m不会被赋值
let {m: name} = {m: 'kingx'};
console.log(name);// kingx
console.log(m); // ReferenceError: m is not defined
下面是可以提高效率的一些对象的解构赋值的应用场景
对象解构时同样可以设置默认值,默认值生效的条件是对应的属性值严格等于undefined
let {m, n = 1, o = true} = {m: 'kingx', o: null};
console.log(m); // kingx
console.log(n); // 1
console.log(o); // null,因为null与undefined不严格相等,默认值并未生效
当属性名和变量名不相同时,默认值是赋给变量的
let {m, n: age = 1} = {m: 'kingx'};
console.log(m); // kingx
console.log(age); // 1
console.log(n); // ReferenceError: n is not defined
嵌套的对象同样可以进行解构,解构时从最外层对象向内部逐层进行,每一层对象值都遵循相同的解构规则
let obj = {
p: [
'Hello',
{y: 'World'}
]
};
let {p: [x, {y: name}]} = obj;
console.log(x); // Hello
console.log(name); // World
console.log(y); // ReferenceError: y is not defined
注意:当父层对象对应的属性不存在,而解构子层对象时,会出错并抛出异常
let obj = {
m: {
n: 'kingx'
}
};
let {o: {n}} = obj;
console.log(n); //TypeError: Cannot match against 'undefined' or 'null'.
假如一个对象有很多通用的函数,在某次处理中,我们只想使用其中的几个函数,那么可以使用解构赋值
let {min, max} = Math;
console.log(min(1,3)); // 1
console.log(max(1,3)); // 3
在上面的实例中,我们只想使用Math对象的min()函数和max()函数,min变量和max变量解构后的值就是Math.min()函数和Math.max()函数,在后面的代码中可以直接使用
当函数的参数是一个复杂的对象类型时,我们可以通过解构去获得想要获取的值并赋给变量
function whois({displayName: displayName, fullName: {firstName: name}}){
console.log(displayName + "is" + name);
}
const user = {
id: 42,
displayName: "jdoe",
fullName: {
firstName: "John", lastName: "Doe"
}
};
whois(user); // jdoe is John
在上面的实例中,whois()函数接收的参数是一个复杂的对象类型,可以通过嵌套的对象解构得到我们想要的displayName属性和name属性。
在ES6中新增了两种运算符,一种是扩展运算符,另一种是rest运算符。这两种运算符可以很好地解决函数参数和数组元素长度未知情况下的编码问题,使得代码能更加健壮和简洁
扩展运算符用3个点表示...
,用于将一个数组或类数组对象转换为用逗号分隔的值序列。
它的基本用法是拆解数组和字符串:
const array = [1, 2, 3, 4];
console.log(...array); // 1 2 3 4
const str = "string";
console.log(...str); // s t r i n g
扩展运算符可以代替apply()
函数,将数组转换为函数参数
比如想要获取数组最大值时,使用apply()
函数的话:
let arr = [1, 3, 5, 8, 2];
console.log(Math.max.apply(null, arr)); // 8
如果使用扩展运算符:
console.log(Math.max(...arr)); // 8
在ES5中,合并数组时,我们会使用concat()
函数:
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
console.log(arr1.concat(arr2)); // [ 1, 2, 3, 4, 5, 6 ]
如果使用扩展运算符:
console.log([...arr1, ...arr2]); // [ 1, 2, 3, 4, 5, 6 ]
Set具有自动的去重性质,我们可以再次使用扩展运算符将Set结构转换成数组
let arr = [1, 2, 4, 6, 2, 7, 4];
console.log([...new Set(arr)]); // [1, 2, 4, 6, 7]
let obj = {name: 'kingx'};
var obj2 = {...obj};
使用扩展运算符对数组或对象进行拷贝时,如果数组的元素或者对象的属性是基本数据类型,则支持深拷贝;如果是引用数据类型,则不支持深拷贝。归根结底是因为引用数据类型的拷贝只是复制了引用的地址,拷贝后的对象仍然共享同一个引用地址。
rest运算符同样使用3个点表示...
,其作用与扩展运算符相反,用于将以逗号分隔的值序列转换成数组
解构会将相同数据结构对应的值赋给对应的变量,但是当我们想将其中的一部分值统一赋给一个变量时,可以使用rest运算符。
let arr = ['one', 'two', 'three', 'four'];
let [arg1, ...arg2] = arr;
console.log(arg1); // one
console.log(arg2); // [ 'two', 'three', 'four' ]
需要注意的是,如果想要使用rest运算符进行解构,则rest运算符对应的变量应该放在最后一位,否则就会抛出异常。因为如果rest运算符不是放在最后一位,变量并不知道要读取多少个数值。
在ES6之前,如果我们不确定传入的参数长度,可以统一使用arguments来获取所有传递的参数:
function foo() {
for (let arg of arguments) {
console.log(arg);
}
}
foo('one', 'two', 'three', 'four');// 输出'one', 'two', 'three', 'four'
函数的参数是一个使用逗号分隔的值序列,可以使用rest运算符处理成一个数组,从而确定最终传入的参数,以代替arguments的使用:
function foo(...args) {
for (let arg of args) {
console.log(arg);
}
}
foo('one', 'two', 'three', 'four');// 输出'one', 'two', 'three', 'four'
通过以上对扩展运算符和rest运算符的讲解,我们知道其实两者是互为逆运算的,扩展运算符是将数组分割成独立的序列,而rest运算符是将独立的序列合并成一个数组。
既然两者都是通过3个点…
来表示的,那么如何去判断这3个点…
属于哪一种运算符呢?我们可以遵循下面的规则:
…
出现在函数的形参上或者出现在赋值等号的左侧,则表示它为rest运算符…
出现在函数的实参上或者出现在赋值等号的右侧,则表示它为扩展运算符模板字符串使用反引号``
括起来,它可以当作普通的字符串使用,也可以用来定义多行字符串,同时支持在字符串中使用${}
嵌入变量
在传统的字符串输出场景中,我们可能会使用加号+
做拼接,但是拼接出来的字符串会丢失掉代码缩进和换行符:
// 传统字符串方案
var str = 'Hello, my name is kingx, ' +
'I am working in Beijng.';
console.log(str); // Hello, my name is kingx, I am working in Beijng.
在上面的实例中,str变量的第一行字符串和第二行字符串之间使用加号进行拼接,并且字符串中有缩进和换行符,但是输出的结果中它们都被忽略了。
而使用模板字符串语法,会保留字符串内部的空白、缩进和换行符:
let str2 = `Hello, my name is kingx,
I am working in Beijng.`;
console.log(str2); // 以下是输出结果
Hello, my name is kingx,
I am working in Beijng.
通过模板字符串的语法输出的字符串包含了缩进和换行符。
字符串变量值传递指的是在想要获取的目的字符串中,会包含一些变量。根据变量的不同可以再细分以下几种场景:
如果字符串中包含了变量,在传统的ES5解决方案中,我们会使用加号拼接变量值:
// 传统解决方案
var name = 'kingx';
var address = 'Beijing';
var str = 'Hello, my name is ' + name + ', ' +
'I am working in ' + address + '.';
console.log(str); // Hello, my name is kingx, I am working in Beijng.
如果在一个复杂的语句中,通过变量拼接,会很容易出错,尤其是遇到单引号和双引号同时出现的场景。
而使用模板字符串语法则不会存在上述问题,模板字符串中不再使用加号进行拼接,而是可以直接嵌入变量,只需要将变量写在${}
之中。如果变量未定义,则会抛出异常。
// 模板字符串方案
let name = 'kingx';
let address = 'Beijing';
let str = `Hello, my name is ${name},
I am working in ${address}.`;
console.log(str);
// 以下是输出结果
Hello, my name is kingx,
I am working Beijng.
在${}
之中不仅可以传递变量,还可以传递任意的JavaScript表达式,包括数学运算、属性引用、函数调用
// 数学运算
let x = 1, y = 2;
console.log(`${x} + ${y * 2} = ${x + y * 2}`); // 1 + 4 = 5
// 属性引用和数学运算
let obj = {x: 1, y: 2};
console.log(`${obj.x + obj.y}`); // 3
// 函数调用
function fn() {
return "Hello World";
}
console.log(`foo ${fn()} bar`); // foo Hello World bar
当传递的变量是一个多层嵌套的复杂引用数据类型值时,模板字符串同样可以支持嵌套解析,遇到表达式会解析成对应的值。
const tmpl = function (addrs) {
return `
${addrs.map(addr => `
${addr.provice}
${addr.city}
`).join('')}
`;
};
const addrs = [{
provice: '湖北省',
city: '武汉市'
}, {
provice: '广东省',
city: '广州市'
}];
console.log(tmpl(addrs));
输出的字符串结果如下所示:
table>
<tr><td>湖北省td>tr>
<tr><td>武汉市td>tr>
<tr><td>广东省td>tr>
<tr><td>广州市td>tr>
table>
在ES6中,增加了一种新的函数定义方式——箭头函数=>
。其基本语法如下所示:
// ES6语法
const foo = v => v;
// 等同于传统语法
var foo = function (v){
return v;
}
最直观的表现是在编写上省去了function关键字,函数参数和普通的函数参数一样,函数体会被一个大括号括起来
const fn = (num1, num2) => {
return num1 + num2;
};
如果函数的参数只有一个,则可以省略小括号;如果函数体只有一行,则可以省略大括号和return关键字
[1, 2, 3].map(r => r * 2); // [2, 4, 6]
// 等同于
[1, 2, 3].map(function(r){
return r * 2;
});
在之前3.6节中关于this的指向问题,得出的结论是this永远指向函数的调用者。但是在箭头函数中,this指向的是定义时所在的对象,而不是使用时所在的对象。
从严格意义上讲,箭头函数中不会创建自己的this,而是会从自己作用域链的上一层继承this。
这里我们通过setTimeout()函数和setInterval()函数来看看普通函数和箭头函数的差别:
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
let timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100); // 3.1秒后输出s1: 3
setTimeout(() => console.log('s2: ', timer.s2), 3100); // 3.1秒后输出s2: 0
在生成Timer的实例timer后,通过setTimeout()函数在3.1秒后输出timer的s1变量,此时setInterval()函数已经执行了3次,由于this.s1++是处在箭头函数中的,这里的this就指向timer,此时timer.s1值为“3”。
而this.s2++是处在普通函数中的,这里的this指向的是全局对象window,实际上相当于window.s2++,结果是window.s2 = 3,而在最后一行的输出结果中,timer.s2仍然为“0”。
通过调用call()
函数与apply()
函数可以改变一个函数的执行主体,即改变被调用函数中this的指向。
但是箭头函数却不能达到这一点,因为箭头函数并没有自己的this,而是继承父作用域中的this。
因此在调用call()函数和apply()函数时,如果被调用函数是一个箭头函数,则不会改变箭头函数中this的指向。
let adder = {
base : 1,
add : function(a) {
var f = v => v + this.base;
return f(a);
},
addThruCall: function(a) {
var f = v => v + this.base;
var b = {
base : 2
};
return f.call(b, a);
}
};
console.log(adder.add(1)); // 2
console.log(adder.addThruCall(1)); // 2
在上面的实例中,执行adder.add(1)时,add()函数内部通过箭头函数的形式定义了f()函数,f()函数中的this会继承至父作用域,即adder,那么this.base = 1,因此执行adder.add(1)相当于执行1 + 1的操作,结果输出“2”。
执行adder.addThruCall(1)时,addThruCall()函数内部通过箭头函数定义了f()函数,其中的this指向了adder。虽然在返回结果时,通过call()函数调用了f()函数,但是并不会改变f()函数中this的指向,this仍然指向adder,而且会接收参数a,因此执行adder.addThruCall(1)相当于执行1 + 1的操作,结果输出“2”。
因此在使用call()函数和apply()函数调用箭头函数时,需要谨慎。
在普通的function()函数中,我们可以通过arguments对象来获取到实际传入的参数值,但是在箭头函数中,我们却无法做到这一点
const fn = () => {
console.log(arguments);
};
fn(1, 2); // Uncaught ReferenceError: arguments is not defined
因为无法在箭头函数中使用arguments,同样也就无法使用caller和callee属性。
虽然我们无法通过arguments来获取实参,但是我们可以借助rest运算符...
来达到这个目的:
const fn = (...args) => {
console.log(args);
};
fn(1, 2); // [1, 2]
箭头函数支持嵌套的写法,假如我们需要实现这样一个场景:有一个参数会以管道的形式经过两个函数处理,第一个函数处理完的输出将作为第二个函数的输入,两个函数运算完后输出最后的结果:
1 const pipeline = (...funcs) =>
2 val => funcs.reduce((a, b) => b(a), val);
3 const plus1 = a => a + 1;
4 const mult2 = a => a * 2;
5 const addThenMult = pipeline(plus1, mult2);
6 addThenMult(5); // 12
在上面的实例中,我们先看第5行代码,这里调用了pipeline()函数,并传入plus1和 mult2两个参数,返回的是一个函数,在函数中使用reduce()函数先后调用传入的两个处理函数。
在执行第6行代码时,pipeline()函数中的val为5,在第一次执行reduce()函数时,a为5,b为plus1()函数,实际相当于执行5 + 1 = 6,并返回了计算结果。
在第二次执行reduce()函数时,a为上一次返回的结果6,b为mult2()函数,实际相当于执行6×2 = 12,因此最后输出“12”
箭头函数并不会绑定this,如果使用箭头函数定义对象字面量的函数,那么其中的this将会指向外层作用域,并不会指向对象本身,因此箭头函数并不适合作为对象的函数。
构造函数是通过new操作符生成对象实例的,生成实例的过程也是通过构造函数给实例绑定this的过程。而箭头函数没有自己的this,因此不能使用箭头函数作为构造函数,也就不能通过new操作符来调用箭头函数。
// 普通函数
function Person(name) {
this.name = name;
}
var p = new Person('kingx'); // 正常
// 箭头函数
let Person = (name) => {
this.name = name
};
let p = new Person('kingx'); // Uncaught TypeError: Person is not a constructor
因为在箭头函数中是没有this的,也就不存在自己的作用域,因此箭头函数是没有prototype属性的。
let a = () => {
return 1;
};
console.log(a.prototype); // undefined
function b(){
return 2;
}
console.log(b.prototype); // {constructor: ƒ}
在给构造函数添加原型函数时,如果使用箭头函数,其中的this会指向全局作用域 window,而并不会指向构造函数,因此并不会访问到构造函数本身,也就无法访问到实例属性,这就失去了作为原型函数的意义
function Person(name) {
this.name = name
}
Person.prototype.sayHello = () => {
console.log(this); // window
console.log(this.name); // undefined
};
let p1 = new Person('kingx');
p1.sayHello();
在上面的代码中,Person()构造函数增加了一个原型函数sayHello(),因为sayHello()函数是通过箭头函数定义的,所以其中的this会指向全局作用域window,从而无法访问到实例的name属性,输出“undefined”。
ES6对数据结构中的对象进行了扩展,包括数据结构本身和对象新增的函数。
传统的JavaScript中,对象都会采用{key: value}的写法,但是在ES6中,可以直接在对象中写入变量,key相当于变量名,value相当于变量值,并且可以直接省略value,通过key表示一个对象的完整属性:
const name = "kingx";
const age = 12;
const obj = {name, age}; //{ name: 'kingx', age: 12 }
// 相当于
const obj = {
name: name,
age: age
};
除了属性可以简写,函数也可以简写,即省略掉关键字function:
const obj = {
method: function(){
return "hello";
}
};
// 等同于
const obj = {
method() {
return "hello";
}
}
到ES6为止,一共有5种方法可以实现对象属性的遍历,具体方法如下所示
for...in
:用于遍历对象自身和继承的可枚举属性(不包含Symbol属性)Object.keys(obj)
:返回一个数组,包含对象自身所有可枚举属性,不包含继承属性和Symbol属性Object.getOwnPropertyNames(obj)
:返回一个数组,包含对象自身所有可枚举属性和不可枚举属性,不包含继承属性和Symbol属性Object.getOwnPropertySymbols(obj)
:返回一个数组,包含对象自身所有Symbol属性,不包含其他属性Reflect.ownKeys(obj)
:返回一个数组,包含自身的可枚举属性、不可枚举属性以及Symbol 属性,不包含继承属性Object.assign()
函数用于将一个或者多个对象的可枚举属性赋值给目标对象,然后返回目标对象。当多个源对象具有相同的属性时,后者的属性值会覆盖前面的属性值。
let target = {a: 1}; //目标对象
let source1 = {b: 2}; //源对象1
let source2 = {c: 3}; //源对象2
let source3 = {c: 4}; //源对象3,和source2对象有同名属性c
console.log(Object.assign(target, source1, source2, source3));
// { a: 1, b: 2, c: 4 }
需要注意的是,Object.assign()
函数无法复制对象的不可枚举属性和继承属性,但可以复制可枚举的Symbol属性
下面是Object.assign()
函数的一些用途
因为Object.assign()
函数可以复制源对象的属性至目标对象中,所以可以实现对象的拷贝。
需要注意的是,使用Object.assign()
函数进行拷贝时,进行的是浅拷贝。如果属性是基本数据类型,则会复制它的值;如果属性是引用数据类型,则会复制它的引用,对源对象属性值进行的修改会影响到目标对象的属性值,两者实际共享同一个对象的引用。
// 传统的写法
function Person(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
}
// Object.assign()写法
function Person(name, age, address) {
Object.assign(this, {name, age, address});
}
当我们采用传统的写法为对象添加公共的函数时,会扩展其prototype属性,使用Object.assign()
函数也可以简化代码编写方式。
// 传统写法
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.getAge = function () {
return this.age;
};
// Object.assign()写法
Object.assign(Person.prototype, {
getName() {
return this.name;
},
getAge() {
return this.age;
}
});
使用Object.assgin()
函数即可以将多个对象合并到某个对象中,也可以将多个对象合并为一个新对象并返回,只需要将target设置为空对象{}
即可
// 多个对象合并到一个目标对象中
const merge =
(target, ...sources) => Object.assign(target, ...sources);
// 多个对象合并为一个新对象并返回
const merge =
(...sources) => Object.assign({}, ...sources);
在传统的JavaScript中,对象的属性名都是由字符串构成的。这样就会带来一个问题,假如一个对象继承了另一个对象的属性,我们又需要定义新的属性时,很容易造成属性名的冲突。 为了解决这个问题,ES6引入了一种新的基本数据类型Symbol,它表示的是一个独一无二的值。
至此JavaScript中就一共存在6种基本数据类型,分别是Undefined类型、Null类型、Boolean类型、String类型、Number类型、Symbol类型
Symbol类型的功能类似于一种唯一标识性的ID,通过Symbol()函数来创建一个Symbol值
let s = Symbol();
在Symbol()函数中可以传递一个字符串参数,表示对Symbol值的描述,主要是方便对不同Symbol值的区分。
但是需要注意的是,由于Symbol值的唯一性,任何通过Symbol()函数创建的Symbol值都是不相同的,即使传递了相同的字符串。
const a = Symbol();
const b = Symbol();
const c = Symbol('one');
const d = Symbol('one');
console.log(a === b); // false
console.log(c === d); // false
Symbol函数并不是一个构造函数,因此不能通过new操作符创建Symbol值
let s1 = new Symbol(); // TypeError: Symbol is not a constructor
Symbol值可以通过toString()函数显示地转换为字符串,但是本身不能参与其他类型值的运算,例如在对Symbol值进行字符串拼接操作时,会抛出异常
let s4 = Symbol('hello');
s4.toString(); // Symbol(hello)
's4 content is: ' + s4; // TypeError: Cannot convert a Symbol value to a string
由于通过Symbol()函数创建的每个值都是不同的,因此如果想使用同一个Symbol值时,需要使用Symbol.for()
函数。
Symbol.for()
函数接收一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值;否则就新建并返回一个以该字符串为名称的Symbol值。
let s1 = Symbol.for('one');
let s2 = Symbol.for('one');
s1 === s2; // true
要注意,Symbol.for()
函数只有在没搜索到时才会新建,而Symbol()函数每次调用都会新建一个值。
由于每一个Symbol值都是不相等的,它会经常用作对象的属性名,尤其是当一个对象由多个模块组成时,这样能够避免属性名被覆盖的情况:
需要遵循的一个原则就是为对象字面量新增属性时需要使用方括号
[]
// 新增一个Symbol属性
let PROP_NAME = Symbol();
// 第一种写法
let obj = {};
obj[PROP_NAME] = "hello";
// 第二种写法
let obj = {
[PROP_NAME]: "hello";
};
// 第三种写法
let obj = {}
Object.defineProperty(obj, PROP_NAME,{
value: "hello";
})
需要注意,不可以通过点运算符为对象添加Symbol属性:
const PROP_NAME = Symbol();
const obj = {};
obj.PROP_NAME = 'Hello!';
console.log(obj[PROP_NAME]); // undefined
console.log(obj['PROP_NAME']); // 'Hello!'
在上面的实例中,我们在通过点运算符为obj增加PROP_NAME属性时,这个PROP_NAME实际是一个字符串,并不是一个Symbol变量。因此我们通过中括号输出PROP_NAME变量对应的值时,得到的是“undefined”;而通过中括号输出’PROP_NAME’字符串值时,得到的是字符串’Hello’。
使用Symbol作为属性名时,不能通过Object.keys()函数或者for…in来枚举,这样我们可以将一些不需要对外操作和访问的属性通过Symbol来定义。