前言
文章为个人原创手写,内容部分参考书籍(《深入理解ES6》)与博客(阮一峰),个人的汇总与总结。
个人汇总的本意是提高自己,同时分享给,0~3工作经验的博友们,方便巩固自己,查漏补缺。
文章不对,希望指出。
ES6简介
ES6,全称ECMAScript 2015,于2015年发布,成为新一代前端标准。值得一提的是,当前的浏览器并不兼容ES6语法。我们需要借助Babel去编译。
块级作用域
我们都知道,变量声明(var)会有变量提升。这里的变量会提前初始化,也可以提前访问。当项目变量复杂的时候,很容易产生bug。es6就在这个时候,引入了let跟const。它解决了下边的问题:
1)局部作用域 新引入的let,const声明,再不会再产生变量提升。避免了变量提前访问的场景,间接的提高了严谨性。我们可以在程序运行时就知道了报错,而非后期的调试中。案例如下
alert("a=" + a);//此时a为underfined,因为变量a已提前声明,但还未赋值
alert("b=" + b);//此时程序已抛出异常,因为此时未找到变量b
var a = 1;
let b = 2;
2)禁止重复声明 如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会导致抛出错误。
3)区分常量与变量 这是let与const的区别。const 声明会阻止对于变量绑定与变量自身值的修改,避免了我们日常开发中,了不小心改到常量的问题。
4)暂时性死区 用案例理解该概念。 for( var i = 0; i<10; i++ ){ setTimeOut( function(){ alert(i ); }, 1000); }
for( let i = 0; i<10; i++ ){
setTimeOut( function(){
alert(i );
}, 1000);
}
通过案例你可以发现,用var声明的,会受setTimeOut所影响,
而let的没有影响。可以说明let有暂时性死区。
冷知识
在 for-in与 for-of 循环中, let 与 const 都能在每一次迭代时创建一个新的绑定,这意味着在循
环体内创建的函数可以使用当前迭代所绑定的循环变量值
(而不是像使用 var 那样,统一使用循环结束时的变量值)。
这一点在 for 循环中使用 let 声明时也成立,不过在 for 循
环中使用 const 声明则会导致错误。
面试考点
简述var,let,const之间的区别?
参考答案:可参考上述解释几个要点。作用域,禁止声明重复,区分常量与变量,暂时性死区。
需要理解,什么是变量作用域的提升。
怎么解决for循环延迟打印?如下案例,怎么让输出正确的结果?
for( var i = 0; i<10; i++ ){
setTimeOut( function(){
alert(i );
}, 1000);
}
参考答案:1.let声明 2.利用闭包 3.立即执行函数
module模块化
为什么要模块化? 在以前,js一直没有模块化的体系。这就会产生一个问题,当项目到达大型时,很大可能性出现方法重叠,以及安全性问题,成为大型项目的一个痛点与障碍。而es6模块化正式为此诞生。
冷知识
-
关于es6模块化的冷知识:
- 模块代码自动运行在严格模式下,并且没有任何办法跳出严格模式
- 在模块的顶级作用域创建的变量,不会被自动添加到共享的全局作用域,它们只会在模块顶级作用域的内部存在;
- 模块顶级作用域的 this 值为 undefined ;
- 模块不允许在代码中使用 HTML 风格的注释(这是 JS 来自于早期浏览器的历史遗留特 性);
- 对于需要让模块外部代码访问的内容,模块必须导出它们;
- 允许模块从其他模块导入绑定。
- 模块化可以用as重命名导出
面试考点
- 模块化引入,跟常规js引入方法有什么不同?
参考答案:
常规js引入,script标签src为"text/javascript"。
而如果你想直接引入模块化js,则src="module"。
可能看到这里,你会反驳,react跟vue打包编译后,
他们模块化后的代码引入的script标签,
为什么没有用到src="module",
那是因为人家已经经过webpack编译后,
已经把你的模块化代码,打包成一个全局js文件中。
- Web 浏览器中的模块加载顺序是怎么样的?
参考答案:
1)当使用模块加载的时候,浏览器立即开始下载模块文件,但并不会执行它,
直到整个网页文档全部解析完为止。
(默认是defer,可以了解一下defer跟async的区别)
2)模块执行的代码,是由上往下的,如果中间包含内联模块,则会优先执行内链模块
- 在es6模块化之前,你还知道那些前端模块化?他们之间有什么区别?
参考答案:
在es6模块化之前,社区还出现了一些模块化的方法,
例如commonJS 和 AMD 。此外还有CMD。
下边我们简述一下他们的概念与区别。
1)AMD, commonJS, 与es6,都属于预加载类型。而后期引入的CDM是懒加载。
何为预加载, 也就是说,在程序调用,所有的模块都加载完成。
而懒加载,是用到什么的时候,才去加载什么。
2)AMD跟cmd专注于前端的规范。而commonjs跟es6 moudle可用于前后端。
3)AMD的代表做为requirejs,cmd的代表作为seajs。commonjs 与 es6,则无需引入,
只需要引入编译器(如babel)即可。 seajs为淘宝引入的规范,我们都知道淘宝相对很大,
不采用懒加载,首屏的时间将会很长,不过现在已经停止维护。
4)es6 跟 commonJS做了如下改变:
1.ES6只能新增值,无法重新赋值就会报错
2.CommonJS 输出是值的拷贝,即原来模块中的值改变不会影响已经加载的该值,
ES6静态分析,动态引用,输出的是值的引用,值改变,引用也改变
,即原来模块中的值改变则该加载的值也改变。
3.CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
4.CommonJS 加载的是整个模块,即将所有的接口全部加载进来,
ES6 可以单独加载其中的某个接口(方法)。
5.CommonJS this 指向当前模块,ES6 this指向undefined
Symbol
Symbol个人也觉得是提高我们项目严谨性的方法之一。 它用于创建不可枚举的属性,并且这些属性在不引用符号的情况下是无法访问的。 只有在访问属性的时候用[]方法才能正确访问,这算不算大大提高了我们程序的严谨性呢?
首先我们理解一下声明是Symbol。Symbol是JS新引入的基本类型。我们都知道在ES5之前,JS 已有的基本类型(字符串、数值、布尔类型、 null 与 undefined )之外, ES6 引入 了一种新的基本类型。
思考一下,为什么要引入这个Symbol呢?
符号起初被设计用于创建对象私有成员,而这也 是 JS 开发者期待已久的特性。 在符号诞生之前,将字符串作为属性名称导致属性可以被轻易 访问,无论命名规则如何。 而“私有名称”意味着开发者可以创建非字符串类型的属性名称,由 此可以防止使用常规手段来探查这些名称。
看到这里,我们结合官方文档。列举一下Symbol常规作用。
应用场景
1.作为内置属性名称
可观察如下案例:
let name = Symbol();
let student = {};
person[name] = "weizhan";
console.log(person[name]); // "weizhan"
此代码创建了一个符号类型的 name 变量,并将它作为 student 对象的一个属性,而每 次访问该属性都要使用这个符号值。 为符号变量适当命名是个好主意,这样你就可以很容易地说明它的含义。
它的好处就是可以避免同参数名的覆盖。从这点,提高了程序的严谨性你是否赞同?因为每一个Symbol都独立存在,及时程序重名了,它也不会覆盖。我们可以通过变量获取它曾经覆过的值。
Symbol 作为对象的属性名,可以保证属性不重名。 用Symbol只能用个[]去访问,没有其他方式可以进行访问。
2.使用Symbol来替代常量
我们可以利用Symbol来创建一些常量。比如订单状态等。
const ORDER_RETRUN = Symbol();//订单退货状态
const ORDER_SUCCESS = Symbol();订单成功状态
const ORDER_PAY = Symbol();//订单支付状态
这一点更体现出程序的简洁性(下文)。如果没有Symbol。 我们只能这样去声明。
const ORDER_RETRUN = "RETRUN";//订单退货状态
const ORDER_SUCCESS = "SUCCESS";订单成功状态
const ORDER_PAY = "PAY";//订单支付状态
我们如果需要保证每个状态都为独立存在,则要保证“RETRUN”,“SUCCESS”,“PAY”不一致。假设出现手误 const ORDER_SUCCESS = "SUCCESS"; const ORDER_PAY = "SUCCESS"; 两者值都为SUCCESS时,两个状态ORDER_SUCCESS跟ORDER_PAY则相等。跟我们原来的单一状态设计相违背。而用Symbol()可直接获取独一无二的值。
看到这里,是否觉得Symbol提高了严谨性。
冷知识点:
关于Symbol的一些额外api,笔者个人是不建议深入的。本文只是列举,有兴趣可自行查询。
- Symbol.hasInstance :供 instanceof 运算符使用的一个方法,用于判断对象继承关系。
- Symbol.isConcatSpreadable :一个布尔类型值,在集合对象作为参数传递给
- Array.prototype.concat() 方法时,指示是否要将该集合的元素扁平化。
- Symbol.iterator :返回迭代器(参阅第七章)的一个方法。
- Symbol.match :供 String.prototype.match() 函数使用的一个方法,用于比较字符串。
- Symbol.replace :供 String.prototype.replace() 函数使用的一个方法,用于替换子字符串。
- Symbol.search :供 String.prototype.search() 函数使用的一个方法,用于定位子字符串。
- Symbol.species :用于产生派生对象(参阅第八章)的构造器。
- Symbol.split :供 String.prototype.split() 函数使用的一个方法,用于分割字符串。
- Symbol.toPrimitive :返回对象所对应的基本类型值的一个方法。
- Symbol.toStringTag :供 String.prototype.toString() 函数使用的一个方法,用于创建对象的描述信息。
- Symbol.unscopables :一个对象,该对象的属性指示了哪些属性名不允许被包含在 with 语句中。
面试考点
怎么访问到Symbol的值?
Symbol 值作为属性名时,该属性是公有属性不是私有属性,可以在类的外部访问。 但是不会出现在 for...in 、 for...of 的循环中,也不会被 Object.keys() 、 Object.getOwnPropertyNames() 返回。如果要读取到一个对象的 Symbol 属性,可以通过 Object.getOwnPropertySymbols() 和 Reflect.ownKeys() 取到。
Array
假设需要创建一个数组,传统的new Array(2);第一个参数,可能为数组长度,也可能是第一个参数值。 设计上没有问题,但是程序中容易出现疏忽造成bug。
如果是克隆多一个数组呢?
而新引入数组方法,array.of跟array.form正是解决该问题。array详细部分,会在第三篇文章具体列出。
面试考点
- 什么为深拷贝,什么为浅拷贝?分别有哪一些呢?
参考答案:
深拷贝,是指完整的新建一个对象。浅拷贝,只是把对象,或者对象首层的属性,指向复制对象中。
所以,深拷贝的对象,值无论怎么变,都只影响只身。而浅拷贝,还会影响以前拷贝的对象。
我们日常见到的拷贝方法,基本都为浅拷贝,比如Object.assign,concat,slice 等。
最简单的深拷贝方法,SON.parse(JSON.stringify(object))。
但是他有弊端,例如:会忽略 undefined,会忽略symbol,
不能序列化函数,不能解决循环引用的对象
当然简单的深拷贝,我们可以直接写一份。
(链接:https://github.com/zhuangweizhan/codeShare)。
但是也会有上述的问题。
如果要完成真正意义上的深拷贝,建议loash库。
- Object.is跟===有什么区别呢?
参考答案:
Object.is() 方法对任何值都会执行严格相等比较,
当在处理特殊的 JS 值时,它有效成为了 === 的一个更安全的替代品。
首先他可以准确判断出NaN。
+0不等于-0。
严格模式下,与非严格模式下,对象重命名有会出现什么情况呢?
参考答案: 非严格模式,值会覆盖。严格模式,程序会报错。
Set与Map的引入
Set与Map是什么东西呢?简单的来说就是集合。
你此时可能会疑问,不是有Object跟Array么。
数组在 JS 中的使用正如其他语言的数组一样,但缺少更多类型的集合导致数组也经常被当作队列与栈来使用。 数组只使用了数值型的索引,而如果非数值型的索引是必要的,开发者便会使用非数组的对 象。
这种技巧引出了非数组对象的定制实现,即 Set 与 Map 。 看到这里或许你还一脸懵逼?好吧。概念看不清楚,我们栗子更好理解一点。请看下边栗子:
关于Map
栗子1:
var map = Object.create(null);
map[1] = "你好"。
alert( map["1"] );
此时,输出"你好"你可能一点都不惊讶。因为你知道,名已经给转发成字符串,即map[1]跟map["1"],已经为同一个对象。但是,如果我们的需求是,键名是任意对象的话,即1跟"1"为不同对象的话,你该怎么处理呢?好吧。Map的优势之一就体现出来。
栗子2:
let map = Object.create(null);
map.count = 1;
if (map.count) {
// ...
}
这段代码通俗易懂?但是你能知道什么意思么?
可以这样理解?判断一个数量count是否为0。 或者也可以这样理解?判断数量count是否存在过。
两个理解,都可能写出这段代码。但是他两个场景,其实都“适用”。但是却都不严谨,他有歧义性。所以,这时候的Map的概念产生的重要原因之一。
其最本质的区别,键值对的集合(Hash 结构),但是传统上只能用字符串当作键。
当然,他有着额外的作用。往下看我会继续讲解到map.
冷知识点
1.map还可以通过数组的形式来新建。比如 const map = new Map([['name', '张三'], ['title', 'Author']]); 等价于 const map = new Map(); map.set('name', '张三') map.set('title', 'Author')
2.Map还带有这些函数:size,has,delete,clear,且提供了三个遍历器生成函数和一个遍历方法:
Map.prototype.keys():返回键名的遍历器。
Map.prototype.values():返回键值的遍历器。
Map.prototype.entries():返回所有成员的遍历器。
Map.prototype.forEach():遍历 Map 的所有成员。
3.对于Map来说,undefined和null是两个不同的键,布尔值true和字符串true是两个不同的键,而NaN之间视为同一个键 ,0和-0也是一个键,
面试考点
下列代码会输出什么呢?
const map = new Map();
map.set(['a'], 1);
map.get(['a'])
答案是underfined。这道题考的是Map的概念理解。Map的键值是针对同一个对象。因为两个['a']均属于新对象,so他们指向不同的内存地址,所以新的获取对象,跟旧的对象无关,输出即为underfined。
WeakMap
WeakMap的设计,是跟Map结构类似,也是用于生成键值对的集合,但是他只能用对象,来作为键值。其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。
首先,他只能作为对象来存值,这个应用场景,是非常至少。我给大家出个需求,可能用得上。
统计该页面所有节点的点击次数。
我们先考虑值存在哪?一个对象,一个数组?好吧,我们避免它键值不能为对象的属性。设计一个非常规数组,或者对象去储存。
const div_a = document.getElementById('a'); const div_b = document.getElementById('b'); const arr = [ [div_a, 3 ], [div_b, 4 ], ];
但我们离开页面的时候(特别是单页面架构),这些节点消失了。之前的统计,将会在页面离开时上报。这时候我们是否需要手动清除arr对象?否则,他将常驻在内存中。
而如果你用WeakMap,但你的页面跳转,对应的dom节点已经消失,这时候对应的统计就会自动消失。是不是很符合场景的使用呢?
但是真正在开发中,WeakMap的确没用过。。。
面试考点
- Map跟WeakMap有什么区别?
参考答案: WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有keys()、values()和entries()方法),也没有size属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。二是无法清空,即不支持clear方法。因此,WeakMap只有四个方法可用:get()、set()、has()、delete()。
Set
Set可能相对更好理解,他可以简单理解为是一个“无重复值”的“有序”列表,且运行值方便快速访问以及判断。
根据概念关键字,你应该可以大概猜到他一部分的作用,就是可以利用他去重。
可以利用他去除重复成员:
let array = ['张三','李四','张三']
[...new Set(array)]
也可以利用他去除重复字符串:
[...new Set('ababbc')].join('')
还有一点很重要,我们可以利用他去接受一些具有 iterable 接口的其他数据结构,例如我们统计页面有几个div?
const set = new Set(document.querySelectorAll('div'));
面试题
let set = new Set();
set.add({});
set.add({});
set.size //
参考答应:值为2. 两个空对象不相等,所以它们被视为两个值。
WeakSet
简单提及一下,看到Map与WeakMap, 也看到Set的讲解,你大胆猜一下WeakSet。
对,就是你想的那样!建议不深挖,还是两个关键字:“对象”,“内存”。有兴趣看看冷知识点。
冷知识点
Weak Set 与正规 Set 的一些共有特征,但是它们还有一些关键的差异,即:
- 对于 WeakSet 的实例,若调用 add() 方法时传入了非对象的参数,就会抛出错误( has() 或 delete() 则会在传入了非对象的参数时返回 false );
- Weak Set 不可迭代,因此不能被用在 for-of 循环中;
- Weak Set 无法暴露出任何迭代器(迭代器问题,下文将重点讲解)(例如 keys() 与 values() 方法),因此没有任何编 程手段可用于判断 Weak Set 的内容;
- Weak Set 没有 forEach() 方法;
- Weak Set 没有 size 属性。
Generator
严格来说generator(生成器)属于ES5,并不是ES6。文章为方便对比Promise,且这部分相信很多朋友并未真正熟悉,特意为一章节。
在讲生成器之前,可能要我们要明白什么是迭代器。
首先,迭代器已经在我们上述讲过的很多章节都使用过,比如 Set 与 Map 。再比如 for-of 与它协同工作,扩展运算符( ... )也使用了它。
我们以for循环来引入迭代器。
var arr = ['a', 'b', 'c'];
for( var i=0; i<arr.length; i++ ){
console.log(arr[i]);
}
这里采用 for 循环的标准方式。这种循环非常直观,然而当它被嵌套使用并要追踪多个变量时,情况就会变得非常复 杂。额外的复杂度会引发错误,而 for 循环的样板性也增加了自身出错的可能性,因为相似 的代码会被写在多个地方。迭代器正是用来解决此问题的。
迭代器,设计每个对象都有next()方法,返回一个上一个循环的结果,以及是否还有下一个操作的done类型(最后一次返回false,其他返回true)。
我们用一段代码来模拟迭代器,可能更方便理解:
function createIterator(items) {
var i = 0;
return {
next: function() {
var done = (i >= items.length);
var value = !done ? items[i++] : undefined;
return {
done: done,
value: value
};
}
};
}
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
生成器( generator )是能返回一个迭代器的函数。生成器函数由放在 function 关键字之 后的一个星号( * )来表示,并能使用新的 yield 关键字。将星号紧跟在 function 关键 字之后,或是在中间留出空格,都是没问题的.
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
Generator有着"停止","开始"的状态,那我们可以用他来控制异步编程,所以,他也是异步的解决方案之一(下方会介绍promise)。
我们写一个最简单的Generator案例:
function* myGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
可见,它不是自动执行,是通过迭代器执行。
冷知识点
上述生成器是通过迭代器一步一步执行,那有没有办法自动执行全部? 这里进行普及。
方案1:Thunk
Thunk 函数是 Generator 函数自动执行的唯一方案。
我们看一下他的设计思维(其实就是遍历帮我我们执行了next):
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
function* g() {
// ...
}
run(g);
当然,这种设计前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在yield命令后面的必须是 Thunk 函数。
方案2:co 模块
如下代码,引入co库,就可以实现我们的自动执行。
var co = require('co');
co(gen);
我们再谈一下他的原理, 先看一下他的简版源码设计:
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}
第一行,检查当前是否为 Generator 函数的最后一步,如果是就返回。
第二行,确保每一步的返回值,是 Promise 对象。
第三行,使用then方法,为返回值加上回调函数,然后通过onFulfilled函数再次调用next函数。
第四行,在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为rejected,从而终止执行。
最后汇总: 自动执行的方案,就是这两种思维: (1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。 (2)Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。
面试考点
for(var i=0;i<arr;i++)跟for(var i of arr )有什么区别呢?
参考答案
这个 for-of 循环首先调用了 values 数组的 Symbol.iterator 方法,获取了一个迭代器 (对 Symbol.iterator 的调用发生在 JS 引擎后台)。接下来 iterator.next() 被调用,迭 代器结果对象的 value 属性被读出并放入了第一个结果变量。
如果你只是简单地迭代数组或集合的值,那么使用 for-of 循环而不是 for 循环就是个好 主意。 for-of 循环一般不易出错,因为需要留意的条件更少;传统的 for 循环被保留用 于处理更复杂的控制条件。
在不可迭代对象、 null 或 undefined 上使用 for-of 语句,会抛出错误。
谈谈你对Generator函数的认识?
参考答案:
首先Generator函数是ES6提供的一种异步编程的解决方案,也是一个状态机,封装了多个内部状态,执行Generator函数会返回一个遍历器对象,依次遍历Generator函数内部的每一个状态。
它的特征是:
(1)function 关键字与函数名之间有一个*号;
(2)Generator函数内部使用yield表达式,定义内部的不同状态;
(3)像调用普通函数那样(函数名后面加个圆括号)调用Generator函数,函数并不会执行,返回的是一个遍历器生成对象,即指向内部状态的指针对象。
Promise
随着越来越多的程序开始使用异步编程,事件与回调函数已不足以支持开发者的所有需求。 Promise 正是为了解决这方面的问题。
Promise 被设计用于改善 JS 中的异步编程,与事件及回调函数对比,在异步操作方面为你提 供了更多的控制权与组合性。 Promise 调度被添加到 JS 引擎作业队列,以便稍后执行。不 过此处有另一个作业队列追踪着 Promise 的完成与拒绝处理函数,以确保适当的执行。
Promise 具有三种状态:挂起、已完成、已拒绝。一个 Promise 起始于挂起态,并在成功时 转为完成态,或在失败时转为拒绝态。在这两种情况下,处理函数都能被添加以表明 Promise 何时被解决。
多数新的 web API 都基于 Promise 创建,并且你可以期待未来会有更多的效仿之作。
我们来详细介绍一下基于 Promise的api:
then : 作用是为 Promise 实例添加状态改变时的回调函数。then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数
catch: Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
finally:
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。
all:
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例
用我们的话来讲: all就是把多个实例绑定成一个。
race:
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。 下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p.then(console.log).catch(console.error);
用我们的话来讲: race就是一个失败了,就代表整个实例失败了。
any:
Promise.any()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。
Promise.any()跟Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束。
用我们的话来讲: 只要有一个实例成功,就代表整个实例成功
面试考点:
Promise的缺陷是什么?
参考答案:
1)无法取消Promise,一旦新建它就会立即执行,无法中途取消。
2)如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
3)当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
4)then的写法相比await,明显在程序代码抒写上,更加繁琐。
下边会输出什么?(考点,promise在自身或者event loop中执行顺序)
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
})
promise.then(() => {
console.log(3);
})
console.log(4);
源码:
有兴趣的了解简版promise源码的:github.com/zhuangweizh…
参考答案:
参考eventLoop的执行顺序细目,答案:1 2 4 3
proxy 跟 Reflect
如果你没深入过proxy,看到proxy,你猜你首先会想起vue-cli 3.0的双向绑定。 那你也能猜到,proxy就是一个代理作用。
首先我们讲诉一下两者的概念
proxy: 代理对目标对象进行了虚拟,因此该代理与该目标对象表面上可以被当作同一个对象来对待。代理允许你拦截在目标对象上的底层操作,而这原本是 JS 引擎的内部能力。拦截行为使用了一个能够响应特定操作的函数(被称为陷阱)。
Reflect: 是给底层操作提供默认行为的方法的集合,这些操作是 能够被代理重写的。每个代理陷阱都有一个对应的反射方法,每个方法都与对应的陷阱函数 同名,并且接收的参数也与之一致。
为什么要引入他们呢?
JS 运行环境包含一些不可枚举、不可写入的 对象属性,然而在 ES5 之前开发者无法定义他们自己的不可枚举属性或不可写入属性。 ES5 引入了 Object.defineProperty() 方法以便开发者在这方面能够像 JS 引擎那样做。 ES6 让开发者能进一步接近 JS 引擎的能力,这些能力原先只存在于内置对象上。语言通过代 理( proxy )暴露了在对象上的内部工作,代理是一种封装,能够拦截并改变 JS 引擎的底 层操作。
还是一脸懵逼?那就看看栗子吧
我提一个简单的需求:我创建一个对象,要求不能插入值为非数字的对象(如果该对象是统计商品价格的话,值要求数字,这样的需求很正常吧?)。
如果在es5,传统思维来讲,你可能会在插入之前,判断一下对象,非数字不让插入。但是,如果在大型项目,你难免会出现少判断的情况吧?有没有办法,从根源上渡劫?
这就需要从JS内置的对象去改变他。而proxy正是符合内置的代理,我们看看代码:
let target = {
"item_00001" : 1000
};
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
if (isNaN(value)) {
throw new TypeError("Property must be a number.");
}
// 添加属性
Reflect.set(trapTarget, key, value, receiver)
}
});
proxy.item_00002 = 599;
console.log(`item_00002:${target.item_00002}`);// item_00002:599
proxy.item_00003 = "a";// 抛出错误
这时候如果插入商品item_00002, 价格为599
我们可以用代理对象赋值,proxy.item_00002=599。而此时代理对象的值,就可以直接影响到原对象的值。即target.item_00002 = 599
但是当我们proxy.item_00003 = "a",此时值不为数字,即抛出异常,那么target的值也不受影响。
通过这个简单的案例,我们就可以清楚的了解proxy的作用,他能拦截并改变 JS 引擎的底层操作。这对大型项目某些字段的严谨性大大提高,提前在浏览器暴露出问题的所在。
知道了proxy的大概作用后,我们来系统了解一下proxy的api
proxy的api
| 代理陷阱 | 被重写的行为 | 默认行为 |
|---|---|---|
| get | 读取一个属性的值 | Reflect.get() |
| set | 写入一个属性 | Reflect.set() |
| has | in | Reflect.has() |
| deleteProperty | delete | Reflect.deleteProperty() |
| getPrototypeOf | Object.getPrototypeOf | Reflect.getPrototypeOf() |
| setPrototypeOf | Object.setPrototypeOf | Reflect.setPrototypeOf() |
| isExtensible | Object.isExtensible | Reflect.isExtensible() |
| preventExtensions | Object.preventExtensions | Reflect.preventExtensions() |
| getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor | Reflect.getOwnPropertyDescriptor() |
| defineProperty | Object.defineProperty | Reflect.defineProperty() |
| apply | Object.defineProperty | Reflect.defineProperty() |
| defineProperty | 调用一个函数 | Reflect.apply() |
| construct | 使用 new 调用一个函数 | Reflect.construct() |
proxy的内置对象大概就这些了。如果你上边你之前都明白上边单词或者语法的意义,相信你已经明白Proxy对应方法的意义。假如还不明白,没关系,我们再来几个栗子:
栗子1
我们可以通过proxy的get来统一返回未知参数,比如未设置过的商品价格统一为"50"。
let target = {
"item_00001" : 1000
};
let proxy = new Proxy(target, {
get(trapTarget, key, receiver) {
if ( receiver[key] == underfined ) {
return "50"
}
// 添加属性
Reflect.get(trapTarget, key, receiver)
}
});
此时proxy["item_00002"]打印将会是50。
栗子2
我们可以通过proxy的has来控制对象的可读性,比如不让别人查询我们的客户是否为vip客户。
let obj = {
userName: 'xiaoming',
vip: ture,
}
let proxy = new Proxy(target, {
has(trapTarget, key) {
if ( key === 'level' ) {
return false
}
return Reflect.has(trapTarget, key)
}
});
此时(vip in proxy) = false
栗子3
我们可以通过proxy的deleteProperty来拒绝对象属性的删除,比如拒绝别人删除我们的年龄。
let target = {
name: "xiaoming",
age: 18
};
let proxy = new Proxy(target, {
deleteProperty(trapTarget, key) {
if (key === "value") {
return ge
} else {
return Reflect.deleteProperty(trapTarget, key);
}
}
});
proxy.deleteProperty(age)//抛出异常