比木白-ES6

ES6语法重点

在最近阅读了《深入理解ES6》这本书之后,发现ES6中有一些不为人所熟悉的部分却成为了提升开发效率的利器,所以将这些语法重点进行规整总结,分享输出给各位小伙伴.

块级作用域

临时死区

ES6中的块级作用域为了防止变量提升,产生了临时死区的方案,也就是说如果你在let、const定义块级作用域变量之前,直接使用此名称的变量,临时死区里面的逻辑就会报错,在执行同步代码度过临时死区之前,使用任何名称的变量都会报错,直到执行过临时死区之后,才会解封.

1
2
3
4
5
6
"use strict"
//这时候a变量不存在变量提升,会报语法错误
console.log(a);
let a = 'a,china,my hometown~';
console.log(a);
//这里打印: a,china,my hometown~

for循环中的块级作用域

ES6中for循环块级作用域中使用的let和const定义变量,和在普通的块级作用域中定义变量不同.for循环块级作用域中是循环的每一次都会声明赋值变量,不是对上次迭代变量的叠加或者累计,而是重新赋值声明,值等于上一次迭代变量加一.在for…in循环获取对象的属性集合项中,也是每一次迭代都会重新声明赋值属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let arr = [];
for(let i = 0; i < 10; i++) {
arr.push(()=>{
console.log(i);
});
}
arr.forEach((item) => {
item();
});
//这里打印:
//0
//1
//2
//3
//4
//5
//6
//7
//8
//9

块级作用域最佳实践

ES6块级作用域最佳实践

尽量在业务开发中,全部使用const定义数据,只有认识到此数据是要进行计算的,就使用let定义数据.

原因

大部分的数据是不可变的,只是作为查询或者展示,而可变的用于前端计算赋值的数据在实际应用场景当中很少很少.

字符串

以码位而非字符来匹配字符

在没有引入Unicode之前,正则表达式使用以一个16位编码也就是两个码位来匹配一个字符,那时候2^16次方个字符在计算机的字符世界中是够用的,但是在引入Unicode之后,2^16次方个字符在诺大的字符集当中非常吃不消,所以正则表达式新增了u描述符,以一个码位也就是32位编码来匹配一个Unicode字符.至此,所有Unicode字符被分为两类,基本平面(BMP)以及辅助平面,BMP是在2^16次方哥字符之内的,而其余超出的则是辅助平面范围

1
2
3
4
5
6
7
8
9
10
11
let str = '𠮷';
console.log(str.length); //2,已超过BMP范围,转以辅助平面
console.log(str.charAt(0)); //无法识别的乱码,因为以BMP的方式去获取字符,超出范围获取到的只能是�
console.log(str.charAt(1)); //还是�
console.log(str.charCodeAt(0)); //55362,不能获取到真实字符的码位,只能获取到码位的一半
console.log(str.charCodeAt(1)); //57271,不能获取到真实字符的码位,只能获取到码位的一半
console.log(String.fromCharCode(55362)); //�
console.log(String.fromCharCode(57271)); //�
console.log(str.codePointAt(0)); //134071,这才是非BMP的辅助平面的方式所匹配的32位码位的真实码位值
console.log(str.codePointAt(1)); //57271,不能获取到真实字符的码位,只能获取到码位的一半
console.log(String.fromCodePoint(134071)); //𠮷,直接使用非BMP的辅助平面的方式可以匹配到真实字符

u描述符

1
2
3
4
5
6
7
let str = '𠮷',
str_double = '𠮷𠮷';
console.log(/\u{20bb7}/.test(str)); //因为\u{20bb7}超过了BMP16位编码的限制,所以是false
console.log(/\u{20bb7}/u.test(str)); //使用u描述符来表述辅助平面的范围,所以是true
console.log(/𠮷/.test(str)); //这里并没有转化成\u{20bb7}BMP16位编码,而是直接用字符去进行匹配,所以返回true
console.log(/𠮷{2}/.test(str_double)); //这里使用了{2}表示将前面的字符乘以2,但这时候就会转化成\u{20bb7}BMP16位编码,超过了BMP16位编码的限制,所以是false
console.log(/𠮷{2}/u.test(str_double)); //使用u描述符来表述辅助平面的范围,所以是true

如果u描述符要兼容老版本浏览器,需要使用try {} catch(err) {},并且使用构造调用,这样是最安全的

1
2
3
4
5
6
7
try {
let str = '𠮷',
reg = new RegExp('\u{20bb7}', 'u');
console.log(reg.test(str));
} catch(err) {

}

各种新增的处理字符串的方法

includes,字符串中是否含有某字符或者某字符串

1
2
3
4
let str = 'hello,world';
console.log(str.includes('o')); //表示此字符串中是否含有o字符,这里返回true
console.log(str.includes('o', 7)); //表示此字符串中从下标为7位置开始直到字符串最后,是否含有o字符,这里返回true
console.log(str.includes('o', 8)); //表示此字符串中从下标为8位置开始直到字符串最后,是否含有o字符,这里返回false

startsWith,字符串中是否以某字符或者某字符串开头

1
2
3
let str = 'hello,world';
console.log(str.startsWith('hello')); //表示此字符串中是否以hello字符串开头,这里返回true
console.log(str.startsWith(',', 5)); //表示此字符串中从下标为5位置开始直到字符串最后是否以,字符开头,这里返回true

endsWith,字符串中是否以某字符或者某字符串结尾

1
2
3
4
5
let str = 'hello,world';
console.log(str.endsWith('ld')); //表示此字符串中是否以ld字符串结尾,这里返回true
console.log(str.endsWith('o', 7)); //表示此字符串中从下标为7位置开始直到字符串开头是否以o字符结尾,这里返回false
console.log(str.endsWith('o', 8)); //表示此字符串中从下标为8位置开始直到字符串开头是否以o字符结尾,这里返回true
console.log(str.endsWith('o', 5)); //表示此字符串中从下标为5位置开始直到字符串开头是否以o字符结尾,这里返回true

repeat,将某字符或者字符串重复n次

1
2
3
let str = 'pet';
console.log(str.repeat(2)); //表示此字符串重复2次输出,这里返回petpet
console.log(str.repeat(5)); //表示此字符串重复5此输出,这里返回petpetpetpetpet

y描述符,粘滞描述符

y粘滞描述符,是区别于g全局描述符,与正则表达式匹配的lastIndex有直接关系,其匹配必须从字符串的开头0下标开始,如果匹配,则从匹配结束的位置开始,再开始匹配,如果再次匹配不到,则回到字符串的开头0下标开始,多次匹配的规则在字符串中必须是连续的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let str = 'hello.hello.hello.hello.hello.',
reg_y = /hello./y,
reg_g = /hello./g;
console.log(reg_g.exec(str)); //这时全局描述符匹配到["hello.", index: 0, input: "hello.hello.hello.hello.hello.", groups: undefined]
console.log(reg_y.exec(str)); //这时粘滞描述符匹配到["hello.", index: 0, input: "hello.hello.hello.hello.hello.", groups: undefined]
console.log(reg_g.lastIndex); //带有全局描述符正则表达式的lastIndex为6
console.log(reg_y.lastIndex); //带有粘滞描述符正则表达式的lastIndex为6
//这时候设置带有全局和粘滞描述符的正则表达式的lastIndex为1
reg_g.lastIndex = 1;
reg_y.lastIndex = 1;
//再去匹配整个字符串
console.log(reg_g.exec(str)); //这时全局描述符匹配到["hello.", index: 6, input: "hello.hello.hello.hello.hello.", groups: undefined]
console.log(reg_y.exec(str)); //这时粘滞描述符匹配到的值为null
//可以看出y粘滞描述符与正则表达式匹配的lastIndex有直接关系,且必须是从lastIndex位置为起点字符串的开头开始能匹配到,才会有匹配值,否则为null,lastIndex会回到源字符串的头部也就是下标为0处,为下次匹配做准备.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let str = 'hello.hello.hello.hello.hello.',
reg_ny = /hello/y,
reg_y = /hello./y,
reg_g = /hello./g;
console.log(reg_g.exec(str)); //这时全局描述符匹配到["hello.", index: 0, input: "hello.hello.hello.hello.hello.", groups: undefined]
console.log(reg_y.exec(str)); //这时粘滞描述符匹配到["hello.", index: 0, input: "hello.hello.hello.hello.hello.", groups: undefined]
console.log(reg_ny.exec(str)); //这时粘滞描述符匹配到["hello", index: 0, input: "hello.hello.hello.hello.hello.", groups: undefined]
console.log(reg_g.lastIndex); //带有全局描述符正则表达式的lastIndex为6
console.log(reg_y.lastIndex); //带有粘滞描述符正则表达式的lastIndex为6
console.log(reg_ny.lastIndex); //带有粘滞描述符正则表达式的lastIndex为5
//再去匹配整个字符串
console.log(reg_g.exec(str)); //这时全局描述符匹配到["hello.", index: 6, input: "hello.hello.hello.hello.hello.", groups: undefined]
console.log(reg_y.exec(str)); //这时粘滞描述符匹配到["hello.", index: 6, input: "hello.hello.hello.hello.hello.", groups: undefined]
console.log(reg_ny.exec(str)); //这时粘滞描述符匹配到的值为null

//可以看出y粘滞描述符多次匹配的规则在字符串中必须是连续的,且必须是从lastIndex位置为起点字符串的开头开始能匹配到,才会有匹配值,否则为null,lastIndex会回到源字符串的头部也就是下标为0处,为下次匹配做准备.

如果y粘滞描述符要兼容老版本浏览器,需要使用try {} catch(err) {},并且使用构造调用,这样是最安全的

1
2
3
4
5
6
7
try {
let str = 'hello.hello.hello.hello.hello.',
reg = new RegExp('hello.', 'y');
console.log(reg.test(str));
} catch(err) {

}

source、flags与sticky

source用于获取正则表达式的除了描述符以外的内容,flags则用于获取正则表达式的描述符,而sticky则是用于获取此正则表达式是否存在y粘滞描述符

1
2
3
4
5
6
let reg_g = /hello./g,
reg_y = /hello./y,
reg_yi = /hello./yi;
console.log(reg_g.source, reg_g.flags, reg_g.sticky); //这里打印: hello. g false
console.log(reg_y.source, reg_y.flags, reg_y.sticky); //这里打印: hello. y true
console.log(reg_yi.source, reg_yi.flags, reg_yi.sticky); //这里打印: hello. yi true

顺便说一下在没有flags来获取正则表达式的描述符时,一般会是这么去封装一个获取正则表达式描述符的方法

1
2
3
4
5
6
function flags(reg) {
let _reg = String(reg),
flagsPosition = _reg.lastIndexOf('/');
return flagsPosition !== -1 ? _reg.slice(flagsPosition + 1) : '';
}
console.log(flags(/hello./yi)); //在这里打印: yi

模板标签

模板标签处理模板字符串的数据参数顺序以及源字符串的操作很便利,你只要写一个函数,并直接放在模板字符串前方,这样就可以直接模板字符串进行处理了,函数的第一个参数是一个数组,表示的是所有除了引入变量以外的字符片段的集合,而后面的参数则是按照引入变量的顺序从左至右的排开,你可以使用不定参数将其转化为数组,不论你怎么引入变量,字符片段的集合数组总是比不定参数转化的数组的长度多一,所以我们可以自己去写一个模板标签,用交织的形式去组合字符串的原顺序或是自定义顺序.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let name = 'Gary',
age = 27,
hobby = 'basketball';
function tags(iterators, ...params) {
let tags_str = '',
iterators_length = iterators.length;
for(let [key, value] of params.entries()) {
tags_str += iterators[key];
tags_str += value;
}
tags_str += iterators[iterators_length - 1];
return tags_str;
}
console.log(tags`我是${name}, 我今年${age}岁了, 我的爱好是${hobby}`);
//在这里打印: 我是Gary, 我今年27岁了, 我的爱好是basketball

还有一种处理源字符串操作的模板标签,可以使源字符串不做转义.那就是String.raw

1
2
3
4
5
6
let name = 'Gary';
console.log(`啊哈,\n${name},原来是你`);
console.log(String.raw`啊哈,\n${name},原来是你`);
//在这里打印: 啊哈,
//Gary,原来是你
//在这里打印: 啊哈,\nGary,原来是你

它可以使换行、制表这些转义符不做转义,我们可以自己写一个和它一致的模板标签,每一个源字符片段都有一个raw的特有属性,其可以将其本身变为不可转义.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let name = 'Gary';
function raw(iterators, ...params) {
let raw_str = '',
iterators_length = iterators.length;
for(let [key, value] of params.entries()) {
raw_str += iterators.raw[key];
raw_str += value;
}
raw_str += iterators.raw[iterators_length - 1];
return raw_str;
}
console.log(`啊哈,\n${name},原来是你`);
console.log(raw`啊哈,\n${name},原来是你`);
//在这里打印: 啊哈,
//Gary,原来是你
//在这里打印: 啊哈,\nGary,原来是你

函数

默认参数

默认参数使得传参更加简化、方便,只有传递的参数为undefined,就给予默认值,如果不为undefined,则进行赋值,类似于for循环let、const每次创建一个声明一样,默认参数也会在函数词法作用域的顶部使用let、const创建一个声明.

1
2
3
4
5
6
7
8
9
10
11
function getPerson(name, age = 25, hobby = 'basketball') {
console.log(`I'm ${name}, ${age} year's old, I love ${hobby}`);
}
getPerson('Gary');
//在这里打印: I'm Gary, 25 year's old, I love basketball
//类似于
//function getPerson(name, _age, _hobby) {
// let age = typeof _age === 'undefined' ? _age : 25;
// let hobby = typeof _hobby === 'undefined' ? _hobby : 'basketball';
// console.log(`I'm ${name}, ${age} year's old, I love ${hobby}`);
//}

甚至可以在非第一个默认参数上面去承接之前定义好的传参或者默认参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getNumber(first = 1, second = first) {
console.log(`first: ${first}, second: ${second}`);
}
getNumber();
//在这里打印: first: 1, second: 1
getNumber(10, 2);
//在这里打印: first: 10, second: 2
getNumber(100);
//在这里打印: first: 100, second: 100
//类似于
//function getNumber(_first, _second) {
// let first = _first || 1;
// let second = _second || first;
// console.log(`first: ${first}, second: ${second}`);
//}

但是默认参数也存在临时死区的,在没有把临时死区当中的变量释放出来之前,直接使用,会引起语法错误.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getNumber(first = second, second = 1) {
console.log(`first: ${first}, second: ${second}`);
}
getNumber();
//在这里会报语法错误,因为在first不传参赋值的情况下,second变量还处于临时死区当中,并未释放,不可以赋值给first
getNumber(10, 2);
//在这里打印: first: 10, second: 2
getNumber(100);
//在这里打印: first: 100, second: 1
//类似于
//function getNumber(_first, _second) {
// let first = _first || _second;
// let second = _second || 1;
// console.log(`first: ${first}, second: ${second}`);
//}

也可使用函数返回值的形式作为默认参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function getNumberOne() {
return 1;
}
function getNumber(first = getNumberOne(), second = first) {
console.log(`first: ${first}, second: ${second}`);
}
getNumber();
//在这里打印: first: 1, second: 1
getNumber(10, 2);
//在这里打印: first: 10, second: 2
getNumber(100);
//在这里打印: first: 100, second: 100
//类似于
//function getNumber(_first, _second) {
// let first = _first || getNumberOne();
// let second = _second || first;
// console.log(`first: ${first}, second: ${second}`);
//}

不定参数

在ES6之前,对于不确定传参的数量的函数,我们基本上使用arguments对象来处理,但是不怎么方便,因为遇到很多情况是,我既有前几个确定的参数,又有后面不确定的参数,所以这时候的处理就需要跳过前几个确定的参数,比如UnderScore.js的pick方法,我们可以来模拟一下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function pick(obj) {
let result = {},
args = Array.prototype.slice.call(arguments, 1);
for(let val of args) {
if(typeof obj[val] !== 'undefined') {
result[val] = obj[val];
}
}
return result;
}
let person = {
name: 'Gary',
age: 25,
hobby: 'My dream is over the world~'
};
console.log(pick(person, 'name', 'hobby'));
//在这里打印: {name: 'Gary', hobby: 'My dream is over the world~'}

在ES6中,为了使这种传参更加方便,引入了不定参数,我们无需对不确定的参数进行特殊处理,还是拿UnderScore.js的pick方法来模拟.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function pick(obj, ...args) {
let result = {};
for(let val of args) {
if(typeof obj[val] !== 'undefined') {
result[val] = obj[val];
}
}
return result;
}
let person = {
name: 'Gary',
age: 25,
hobby: 'My dream is over the world~'
};
console.log(pick(person, 'name', 'hobby'));
//在这里打印: {name: 'Gary', hobby: 'My dream is over the world~'}

不定参数必须以函数参数的最后位置出现,如果后方位置还有参数,则会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function pick(obj, ...args, 'age') {
let result = {};
for(let val of args) {
if(typeof obj[val] !== 'undefined') {
result[val] = obj[val];
}
}
return result;
}
let person = {
name: 'Gary',
age: 25,
hobby: 'My dream is over the world~'
};
console.log(pick(person, 'name', 'hobby'));
//在这里就会报语法错误

展开运算符

在ES6之前,有一些内置函数和对象比如说Math.max不支持以数组传入参数,只能一个一个的传入,所以我们这时候就需要使用apply的形式,直接传入数组,然后转化为参数.

1
2
3
4
5
6
7
let value = 100,
value_ano = 120;
console.log(Math.max(value, value_ano));
//在这里打印: 120
let value_arr = [0, 99 ,-10, 100, 1000, 2000, 200, 1];
console.log(Math.max.apply(null, value_arr));
//在这里打印: 2000

在ES6推出了展开运算符之后,这种情况发生了改变,我们不需要使用apply的这种形式进行显式绑定.

1
2
3
let value_arr = [0, 99 ,-10, 100, 1000, 2000, 200, 1];
console.log(Math.max(...value_arr));
//在这里打印: 2000

有了展开运算符和不定参数的支持,在函数传参方面大为改观,我们现在可以使用这两种新形势进行配合写一个bind显式绑定方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
if(!Function.prototype.bind) {
Function.prototype.bind = function (context, ...params) {
let self = this;
function F() {}
let fBind;
fBind = function (...args) {
//这里做判断是为了防止返回的函数,使用new构造调用.new绑定的优先级高于其他绑定
return self.apply(this instanceof fBind ? this : context, [...params, ...args])
};
F.prototype = self.prototype;
fBind.prototype = new F();
return fBind;
};
//实验一下
let obj = {name: 'Gary', age: 25};
function Person(name, age) {
console.log(this.name);
this.name = name;
console.log(this.name);
console.log(this.age);
this.age = age;
console.log(this.age);
}
Person.prototype.introduce = function () {
console.log(`I'm ${this.name}, ${this.age} year's old`);
};
let func = Person.bind(obj, 'Tom');
func(28);
//在这里打印:
//Gary
//Tom
//25
//28
let _func = Person.bind(obj);
let quote = new _func('Simon', 30);
console.log(quote.name, quote.age);
quote.introduce();
//在这里打印:
//undefined
//Simon
//undefined
//30
//Simon 30
//I'm Simon, 30 year's old
}

new.target

函数在普通调用和构造调用时,所使用的内置函数是不同的,普通调用使用的是[[Call]]内置函数,而构造调用使用的是[[Constructor]],而在一些必须使用构造调用的函数的判断就成了问题.在ES6之前是这样判断的

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person (name, age) {
if(!(this instanceof Person)) {
throw new Error('此函数必须使用构造调用~');
} else {
this.name = name;
this.age = age;
}
}
let gary = new Person('Yinwenkai', 26);
console.log(gary.name, gary.age);
//在这里打印: Yinwenkai 26
let simon = Person('Yinwenkai', 27);
//在这里就会报语法错误,因为函数直接调用,使用的是[\[Call\]]内置函数,并不是[\[Constructor\]],这边就会抛出错误,this指向window或者global

但是这样还是会有漏洞的,我们可以使用硬绑定,将Person硬绑定至构造调用出来的引用对象上面.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person (name, age) {
if(!(this instanceof Person)) {
throw new Error('此函数必须使用构造调用~');
} else {
this.name = name;
this.age = age;
}
}
let gary = new Person('Yinwenkai', 26);
console.log(gary.name, gary.age);
//在这里打印: Yinwenkai 26
let simon = Person('Yinwenkai', 27);
//在这里就会报语法错误,因为函数直接调用,使用的是[\[Call\]]内置函数,并不是[\[Constructor\]],这边就会抛出错误,this指向window或者global
let lily = Person.call(gary, 'Yinwenkai', 30);
//在这里就不会报任何错误,因为硬绑定到构造调用出来的引用对象上面,就可以普通调用了.

所以ES6推出了new.target,此对象总是指向构造调用函数本身的,也就是说使用[[Constructor]]去调用才会有此对象,如果使用[[Call]]去调用就不会有此对象也就等于undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person (name, age) {
if(new.target === Person) {
this.name = name;
this.age = age;
} else {
throw new Error('此函数必须使用构造调用~');
}
}
let gary = new Person('Yinwenkai', 26);
console.log(gary.name, gary.age);
//在这里打印: Yinwenkai 26
let simon = Person('Yinwenkai', 27);
//在这里就会报语法错误,因为函数直接调用,使用的是[\[Call\]]内置函数,并不是[\[Constructor\]],这边就会抛出错误,this指向window或者global
let lily = Person.call(gary, 'Yinwenkai', 30);
//在这里还是会报语法错误,因为new.target不管函数如何硬绑定,它也只在使用[[Constructor]]去调用函数的时候,才是存在的

块级函数

ES6中,因为有了块级作用域,所以对函数的声明限制在严格模式下做了强控,由此有了块级函数,只能在此块级作用域中可访问并调用到此函数,全局并不能访问到并调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"use strict"
if (true) {
function foo() {
console.log('this is foo func~');
}
console.log(foo);
foo();
}
console.log(foo);
foo();
//在这里打印:
//ƒ foo() {
// console.log('this is foo func~');
//}
//this is foo func~
//下面就会报语法错误
//VM1182:9 Uncaught ReferenceError: foo is not defined
//at <anonymous>:9:17

但是如果不是在严格模式下,不仅在此块级作用域中可访问并调用到此函数,全局依然可以访问并调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (true) {
function foo() {
console.log('this is foo func~');
}
console.log(foo);
foo();
}
console.log(foo);
foo();
//在这里打印:
//ƒ foo() {
// console.log('this is foo func~');
//}
//this is foo func~
//ƒ foo() {
// console.log('this is foo func~');
//}
//this is foo func~

name

说到name,就不得不说它被开创的原因,它本来是为了追溯堆栈信息而生,就是当出现某些错误的时候,比如TypeError、ReferenceError和SyntaxError时,引擎就会根据此函数的name查找所在函数的发生错误的位置,一层层寻找下去,形成函数调用时追溯错误信息的堆栈.而name就成了函数的名称,length就成了函数参数的个数.

1
2
3
4
5
6
7
8
function person(name, age) {
console.log(name, age);
}
console.log(person.name);
console.log(person.length);
//在这里打印:
//person
//2

不同方式生成的函数的名称是不同的,比如说bind硬绑定,它所返回的函数的名称就是”bound [[函数真实名称]]”,而使用Function构造出来的函数的名称则是”anonymous”.

1
2
3
4
5
6
7
function person(name, age) {
console.log(name, age);
}
const gary = person.bind(null, 'Gary');
console.log(gary.name);
//在这里打印
//bound person
1
2
3
4
const person = new Function();
console.log(person.name);
//在这里打印
//anonymous

箭头函数

ES6中最让人感兴趣之一,也是变化最大之一的改动就是箭头函数了.特点在于它的方便和简化,它废弃了传统JS函数中的一些复杂而又让人费解的特性,其实际多应用在回调函数、Promise处理异步问题的结构中.
特性:

  1. 不可使用new绑定构造调用,就是说箭头函数里就无[[Constructor]]内置属性
  2. 不可改变函数this的指向,this永远指向外层词法作用域
  3. 不可在函数内部使用arguments,也就是说arguments在箭头函数里面压根儿不存在
  4. 不可在函数内部使用super,既然不能new绑定构造调用,那肯定也不能使用继承了,更不能使用super
  5. 不可使用new.target,既然不能new绑定构造调用,那肯定也用不了new.target,箭头函数里就无[[Constructor]]内置属性,而new.target又与[[Constructor]]内置属性相关

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let name = 'Simon';
    const obj = {
    name: 'Gary',
    getName: () => {
    console.log(this.name);
    }
    };
    const simon = {
    name: 'Simon'
    };
    console.log(obj.getName());
    console.log(obj.getName.call(simon));
    //在这里打印
    //undefined
    //undefined
    1
    2
    3
    4
    5
    6
    7
    const Person = (name, age) => {
    this.name = name;
    this.age = age;
    };
    const aaron = new Person('aaron', 25);
    //在这里会报类型错误
    //Uncaught TypeError: Person is not a constructor
1
2
3
4
5
6
7
const Person = name => {
console.log(name);
console.log(arguments[0]);
}
Person('Alice');
//在这里直接报引入错误
//VM74:3 Uncaught ReferenceError: arguments is not defined
1
2
3
4
5
6
7
8
9
10
11
const Person = name => {
if(new.target === Person) {
this.name = name;
} else {
throw new TypeError('此函数必须使用构造调用');
}
};
const person = new Person('比木白');
console.log(person.name);
//在这里直接报引入语法错误
//Uncaught SyntaxError: new.target expression is not allowed here

尾调用优化

其实尾调用优化的定义非常简单,目标也很清楚,就是一句话:循环利用某一堆栈,让我们先看一张图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function baz() {
const a = 'Gary';
function bar() {
const b = 'Simon';
function foo() {
const c = 'Lily';
console.log(a, b, c);
}
foo();
}
bar();
}
baz();
//在这里打印:
//Gary Simon Lily

上述代码和图全面阐述了非尾调用时,函数堆栈执行的情形,当baz函数执行时,会将此函数推到内存的,但是baz函数不会因为执行调用而被销毁,因为内部还存在bar函数,等待bar函数执行完毕之后,baz才会从堆栈栈底销毁,而bar函数的执行,又会产生推到内存栈底的函数,以此类baz -> bar关系推bar -> foo是一样的,堆栈随着内部函数的调用不断累积,且不能第一时间被销毁,必须等待内部的堆栈调用完全销毁才能轮到自己.这种不可复用的堆栈使用,ES6给出了优化功能,那就是尾调用.

尾调用的条件

  1. 内部函数的调用必须在外层函数结构体的最后一行执行,并且进行返回.
  2. 不可在内部函数结构体中,使用任何外层函数结构体中的变量、方法,也就是说不能存在闭包或者词法作用域链等行为.
  3. 内部函数的调用必须在外层函数结构体的最后一行不可进行显式计算返回.

    让我们举一个非尾调用优化的例子,来个阶乘吧

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function factorial(n) {
    if(n <= 1) {
    return 1;
    } else {
    return n * factorial(n - 1);
    }
    }
    factorial(5);
    //在这里打印:
    //120

    再举一个阶乘尾调用优化的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function factorial(n, total = 1) {
    if(n <= 1) {
    return total;
    } else {
    return factorial(n - 1, total * n);
    }
    }
    factorial(5);
    //在这里打印:
    //120

    这就是尾调用优化,符合一开始我所说的是三个条件,递归调用函数是在函数结构体的最后一行,且并没有进行显式计算,不存在闭包和函数作用域链使用任何外层函数结构体中的变量、方法的情况.