let 和 const
关于 let 和 const 我们需要知道的一些语法如下:
变量提升
变量可以在声明之前使用,值为 undefined
,这种现象叫做变量提升。
var 命令存在变量提升
let 和 const 不存在,其声明的变量一定要再声明后使用,否则报错。
暂时性死区
ES6 明确规定,如果区块内存在 let 或 const 命令,则这个区块对这些命令声明的变量从一开始就形成了封闭的作用域,只要在声明之前使用这些变量就会报错。
所以在代码块内,使用let 变量在声明之前,该变量都是不可用的,这种情况叫做 暂时性死区
let 和 const 存在暂时性死区
const 声明的变量值是否可以改动
const 实际上保证的是变量指向的内存地址不能改动,所以对于简单数据类型而言,值就保存在变量指向的内存地址中,因此等同于常量,但是对于引用数据类型来说,变量指向的内存地址保存的只是一个指针,不是具体的值,const 只能保证这个指针是固定的,但是指针指向的数据结构是不是可变的就不是 const 能控制的了,所以我们在声明一个对象为 const 时需要非常小心。
如果我们想冻结一个对象呢,可以使用 Object.freeze
方法:
const foo = Object.freeze({});
并且对象自身的属性也应该冻结,可以使用下面的彻底冻结对象的方法:
let constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach((key, i) => {
if (typeof obj[key] === 'object') {
constantize(obj[key]);
}
})
}
变量的解构赋值
ES6 引入的解构赋值功能大大的减少了代码量,但是解构赋值仅能对具有迭代器 (Iterator) 的数据使用。
Iterator 迭代器
Iterator 作为一种统一的接口机制供我们调用处理不同的数据结构,让可以用相同的方式来遍历集合,而不用去考虑集合的内部实现,若数据的形式发生改变,只要数据内部还存在 System.iterator 属性,那遍历的代码就可以不用做修改直接使用。 同时,其服务于 for...of 循环, for...of 又有效地避免了以往对数据集仅能通过 forEach 、 for..in 遍历时遇到的一部分问题。
数组的解构赋值
let [a, b, c] = [1, 4, 6]; // a:1 b:4 c:6
let [foo, [[bar], baz]] = [1, [[2], 3]]; // foo:1 bar:2 baz:3
let [head, ...tail] = [1, 2, 3, 4]; // head:1 tail:[2,3,4]
// 如果解构不成功,变量的值为 undefined
let [x, y, ...z] = ['a']; // x:a y:undefined z:[]
// 如果等号的右边不是可遍历的解构(不具有Iterator接口),那么将会报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = {};
let [foo] = null;
// 以上语句都会报错
解构赋值的默认值
let [foo = true] = []; // foo:true
// 这句话表示,若 foo 为空则默认为 true
let [x, y = 'b'] = ['a']; // x:'a' y:'b'
let [x, y = 'b'] = ['a', 'undefined']; // x:'a' y:'b'
ES6 内部使用严格相等(===)来判断一个位置是否有值,所以如果一个数组成员不严格等于 undefined,默认值不会生效。
let [x = 1] = [null]; // x:null
let [x = 1] = [undefined]; x:1
上面的代码,如果一个数组的成员是 null,默认值不会生效,因为 null 不严格等于 undefined。
如果默认值是一个表达式,那么这个表达式是惰性求值的,意思是只有在用到的时候才会求值。
function fn(){
return 'xxx';
}
let [x = fn()] = [1];
比如说上面的代码,因为 x 可以取到非 undefined 得值,所以 fn 函数不会执行。
并且默认值可以使用解构赋值的其他变量,但是该变量必须已经声明。
let [x = y, y = 1] = []; // 报错
let [x = 1, y = x] = []; // x:1 y:1 不报错
对象的解构赋值
对象的解构与数组有一个重要的不同:
数组的元素是按次序排列的,变量的取值是由它的位置决定的,但是对象的属性没有次序,变量必须与属性同名才能取到正确的值
let { bar, foo, baz } = {
foo: 'aaa',
bar: 'bbb'
}
// bar:'bbb' foo:'aaa' baz:undefined
如果变量名和属性名不一致,那就必须写成下面这样
对象的解构赋值的内部机制是先找到同名属性,然后再赋值给对于的变量,所以真正被定义赋值的是后者,不是前者,所以我们新定义的那个变量要写在冒号的右边
let { kkk: hello, foo } = {
foo: 'aaa',
bar: 'bbb',
kkk: 'ccc'
} // hello:'ccc' foo:'aaa'
同样的,对象的赋值解构也可以指定默认值,生效的条件也同样是对象的属性值严格等于 undefined。
字符串 数值 布尔值的解构赋值
字符串也可以解构赋值,因为此时字符串被转换成了一个类似于数组的对象。
const [a, b, c] = 'hello'; // a:'h' b:'e' c:'c'
解构赋值时,如果等号的右边是数值类型或者布尔值类型,则会先转换成对象
使用场景
- 交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
- 从函数返回多个值
function example () {
return [1, 2, 3];
}
let [a, b, c] = example();
- 函数参数的定义
function f ([x, y, z]) { };
f([1, 2, 3]);
- 提取 JSON 数据
let jsonData = {
name: 'echo',
age: '19',
status: 'ok'
}
let { name, status: type, age } = jsonData;
// name:echo type:'ok' age:'19'
- 输入模块的指定方法
我们在加载模块的时候,往往只需要引入特定的几个方法,这种时候赋值解构就很好用。
const { sourceMap, sourceNode } = require('source-map');
字符串扩展方法
我们知道在 ES5 及之前只有 indexOf 方法可以确定一个字符串是否包含在另一个字符串内,ES6 这里有提供了 3 中新的方法
- includes() - 返回布尔值,表示是否找到了参数字符串
- startsWith() - 返回布尔值,表示参数字符串是否是源字符串的头部
- endsWith() - 返回布尔值,表示参数字符串是否是源字符串的尾部
以上 3 个方法都支持第二个参数,表示开始搜索的位置。
还有其他的扩展方法如下:
- repeat() - 返回一个新字符串,表示将原字符串重复 n 次,参数如果是小数将会被取整,负数或者 infinity 将会报错
'x'.repeat(3) // xxx
- padStart() - 用于头部补全,如果省略第二个参数,则会用空格补全,如果原字符串的长度大于或者等于指定的最小长度,返回原字符串
- padEnd() - 用于尾部补全,如果省略第二个参数,则会用空格补全,如果原字符串的长度大于或者等于指定的最小长度,返回原字符串
'x'.padStart(5, 'ab'); // ababx
'x'.padEnd(5, 'ab'); // xabab
'xxx'.padStart(2, 'ab'); // xxx
另外还提供了 模板字符串的方法,用得比较多,所以这里略过。
数值的扩展方法
Number.isFinite() 和 Number.isNaN()
Number.isFinite() 用于检查一个数值是否是有限的
Number.isFinite(1.8) // true
Number.isFinite(NaN) // false
Number.isNaN() 用于检查一个值是否为 NaN
首先我们知道 NaN === NaN 这个等式会返回 false
Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.parseInt() 和 Number.parseFloat()
ES6 将 parseInt() 和 parseFloat() 移植到了 Number 对象上,行为完全保持不变
Number.isInteger()
Number.isInteger() 用来判断一个值是否为整数,由于 JavaScript 中整数和浮点数的储存方法是一样的,所以 3 和 3.0 是同一个值。
安全整数和 Number.isSafeInteger()
我们知道 JavaScript 能够准确表示的整数范围在 -2^53 到 2^53 之间(不含两个端点),超过这个范围就语法精确表示了。
ES6 中引入了这两个最大值和最小值作为常量,Number.isSafeInteger() 这个方法则是能够用来判断一个整数是否落在这两个数的范围内。
在 Math 对象上的扩展
这里写得很详细~~
函数的扩展
函数的默认参数值
ES6 允许为函数的参数设置默认值,直接卸载参数定义的后面即可
function log (x, y = 'hello') {
console.log(x, y);
}
同时参数默认值也可以和解构赋值结合使用
function log ({ x, y = 'hello' }) {
console.log(x, y);
}
log({}); // undefined, 'hello'
log({ x: 1 }); // 1,'hello'
log(); // 报错
箭头函数
ES6 允许使用箭头定义函数,这个使用广泛了... 就不花篇幅介绍了。
另外,箭头函数可以绑定 this 对象,大大减少了使用 call / apply / bind 之类的显式绑定 this 对象的写法。
绑定 this
函数绑定运算符是并排的双冒号( :: ),双冒号左边是一个对象,右边是一个函数,该运算符会自动将左边的对象作为 this 对象绑定到右边的函数上
foo::bar;
// 等价于
bar.bind(foo);
尾调用优化
尾调用是函数式编程的一个重要概念,其概念非常简单,即 某个函数的最后一步是调用另一个函数,例如:
function f (x) {
return g(x);
}
像上面这样,函数的最后一步是调用函数 g,这样的就叫做尾调用。
下面这样的都不属于尾调用:
function f (x) {
let y = g(x);
return y;
}
function f (x) {
return g(x) + 1;
}
function f (x) {
g(x);
// 这种写法等价于
g(x);
return undefined;
}
尾递归
函数自身调用自身称为递归,如果尾调用自身就叫做尾递归
递归非常的耗费内存,因为需要保存成千上百个调用帧,很容易发生栈溢出,但是对于尾递归来说,由于只存在一个调用帧,所以不会发生栈溢出。
这里有个非常经典的例子就是斐波那契数列,我们先来看看非尾递归的 Fibonacci 数列实现如下:
function Fibonacci (n) {
if (n <= 1) {
return 1;
}
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
// Fibonacci(10) => 89
// Fibonacci(100) => 堆栈溢出
尾递归优化后的 Fibonacci 数列实现如下
function Fibonacci (n, ac1 = 1, ac2 = 1) {
if (n <= 1) {
return ac2;
}
return Fibonacci(n - 1, ac2, ac1 + ac2);
}
数组的扩展
扩展运算符
扩展运算符是三个点(...),它的作用是将一个数组转为用逗号分割的参数序列
console.log(...[1, 2, 3]) // 1 2 3
Array.from() 和 Array.of()
Array.from() 方法用于将两类对象转为真正的数组,类似数组的对象和可遍历的对象(比如 Set 和 Map)。
例子如下:
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
}
// es5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// es6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
Array.of() 方法用于将一组值转换为数组:
Array.of(3, 11, 8); // [3,11,8]
Array.of(3); // [3]
Array.of(); // []
Array.of() 方法主要是用来弥补 Array() 的不足,因为 Array() 的参数个数不同会导致 Array() 的行为有差异。
Array(); // []
Array(3); // [, , ,]
Array(2, 11, 8); // [2,11,8]
上面的代码中,Array 方法没有参数,有 1 个参数或多个参数时,返回的结果都不一样,只有当参数个数不少于 2 个的时候,Array 才回返回由参数组成的新数组,参数个数只有 1 时,实际上是指定数组的长度。
数组实例的 find() / findIndex() / fill()
数组的一些遍历方法之前写过,find() / findIndex()里面都有,指路~
fill() - 该方法使用指定值填充一个数组
['a', 'b', 'c'].fill(7); // [7,7,7]
new Array(3).fill(7); // [7,7,7]
// 如上,fill 方法用于空数组的初始化时非常方便,数组中已有的元素会被全部抹去。
// fill 方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
['a', 'b', 'c'].fill(7, 1, 2); // ['a',7,'c']
数组实例的 entries() / keys() / values()
ES6 提供了 3 个新方法:entries() / keys() / values() 用于遍历数组,它们均返回一个遍历器对象,可以用 for...of 循环遍历.
唯一的区别在于:
keys() 是对键名的遍历
values() 是对键值的遍历
entries() 是对键值对的遍历
for (let [index, ele] of ['a', 'b'].entries()) {
console.log(index, ele);
// 0 'a'
// 1 'b'
}
for (let ele of ['a', 'b'].values()) {
console.log(ele);
// 'a'
// 'b'
}
for (let index of ['a', 'b'].keys()) {
console.log(index);
// 0
// 1
}
数组实例的 includes()
includes() 方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的 includes() 方法类似,该方法的第二个参数表示搜索的起始位置,默认为 0。如果第二个参数为负数,则表示倒数的位置。
[1, 3, 5].includes(3); // true
[1, 3, NaN].includes(NaN); // true
[1, 3, 7].includes(3, 3); // false
[1, 3, 7].includes(7, -1); // true
对象的扩展
首先 ES6 允许直接写入变量和函数作为对象的属性和方法,使书写更加的简洁,即在对象中只写属性名不写属性值,这时的属性值等于属性名所代表的的变量。
let foo = 'hello';
let obj = {
foo,
kk: 'kkk'
};
// obj => {foo:'hello',kk: 'kkk'}
Object.is()
ES5 比较两个值是否相等只有两个运算符:
- == 相等运算符:缺点是会自动转换数据类型
- === 严格相等运算符: 缺点是 NaN 不等于自身,以及 +0 等于 -0
但是我们有时候需要在所有环境中,只要两个值是一样的,它们就应该相等。
所以 ES6 提出了 同值相等
算法来解决这个问题,Object.is() 就是这个新方法,它用来比较两个值是否严格相等,与严格相等运算符 === 行为基本一致。不同之处只有两个:
- +0 不等于 -0
- NaN 等于自身
Object.assign()
Object.assign() 用于将源对象的所有可枚举属性复制到目标对象,其方法的第一个参数时目标对象,后面的参数都是源对象(如果目标对象和源对象有同名属性,则后面的属性会覆盖前面的)
let target = {
a: 1
}
let source1 = {
b: 2,
c: 4
}
let source2 = {
b: 3
}
Object.assign(target, source1, source2);
// {a: 1, b: 3, c: 4}
需要注意的是,如果其他类型的值(数值,字符串,布尔)不在首参数也不会报错,但是除了字符串会以数组的形式复制到目标对象,其他值都不会产生效果。
另外需要注意的是,Object.assign() 实行的是浅拷贝。所以如果源对象某个属性值是对象,那么目标对象复制得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上。
Object.assign() 方法的用途有很多:
- 给对象添加属性
class Point {
constructor(x, y) {
Object.assign(this, { x, y });
}
}
// 上面的方法通过 assign 方法将 x 属性和 y 属性添加到了 Point 类的对象实例中。
- 给对象添加方法
Object.assign(SomeClass.prototype, {
someMethod (arg1) {
}
});
- 克隆对象 / 合并多个对象
function clone (origin) {
return Object.assign({}, origin);
}
Set
Set 是 ES6 提供的新的数据结构,它类似于数组,但是成员的值都是唯一的,没有重复的值。Set 它本身是一个构造函数,用来生产 Set 数据结构,Set 的实例化时可以接受一个数组或者具有 iterable 接口的其他数据结构作为参数。
let s = new Set();
Set 的属性和方法
- Set.prototype.constructor —— 构造函数,默认就是 Set 函数
- Set.prototype.size —— 返回 Set 实例的成员总数
- add(value) —— 添加某个值,返回 Set 结构本身
- delete(value) —— 删除某一个值,返回一个布尔值表示是否删除成功
- has(value) —— 返回一个布尔值,表示是否删除成功
- clear() —— 清楚 Set 中的所有成员,没有返回值
并且 Array.from 可以将 Set 结构转为数组
Set 的遍历操作
Set 结构的实例为我们提供了 4 个遍历方法:
- keys() —— 遍历返回键名
- values() —— 遍历返回键值
- entries() —— 返回键值对遍历
- forEach() —— 对每个成员使用回调函数
但是呢,由于 Set 对象没有键名,只有键值(或者说键名和键值是同一个值),所以 keys 和 values 方法的行为完全一致。