目录
8 举一些ES6对Function函数类型做的常用升级优化?
20 module、export、import是什么,有什么作用?
21 日常前端代码开发中,有哪些值得用ES6去改进的编程优化或者规范?
26 Object.is() 与原来的比较操作符 ===、== 的区别?
32 全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?
33 介绍下 Set、Map、WeakSet 和 WeakMap 的区别
34 Promise.all() 和 Promise.allSettled()的比较
ES2015特指在2015年发布的新一代JS语言标准,ES6泛指下一代JS语言标准,包含ES2015、ES2016、ES2017、ES2018等。现阶段在绝大部分场景下,ES2015默认等同ES6。ES5泛指上一代语言标准。ES2015可以理解为ES5和ES6的时间分界线
babel是一个ES6转码器,可以将ES6代码转为ES5代码,以便兼容那些还没支持ES6的平台
在
ES6之前,声明变量只能用var,var方式声明变量其实是很不合理的,准确的说,是因为ES5里面没有块级作用域是很不合理的。没有块级作用域回来带很多难以理解的问题,比如for循环var变量泄露,变量覆盖等问题。let声明的变量拥有自己的块级作用域,且修复了var声明变量带来的变量提升问题。
优化部分
ES6新增了字符串模板,在拼接大段字符串时,用反斜杠()`取代以往的字符串相加的形式,能保留所有空格和换行,使得字符串拼接看起来更加直观,更加优雅
升级部分
ES6在String原型上新增了includes()方法,用于取代传统的只能用indexOf查找包含字符的方法(indexOf返回-1表示没查到不如includes方法返回false更明确,语义更清晰), 此外还新增了startsWith(),endsWith(),padStart(),padEnd(),repeat()等方法,可方便的用于查找,补全字符串
优化部分
- 数组解构赋值。
ES6可以直接以let [a,b,c] = [1,2,3]形式进行变量赋值,在声明较多变量时,不用再写很多let(var),且映射关系清晰,且支持赋默认值 - 扩展运算符。
ES6新增的扩展运算符(...)(重要),可以轻松的实现数组和松散序列的相互转化,可以取代arguments对象和apply方法,轻松获取未知参数个数情况下的参数集合。(尤其是在ES5中,arguments并不是一个真正的数组,而是一个类数组的对象,但是扩展运算符的逆运算却可以返回一个真正的数组)。扩展运算符还可以轻松方便的实现数组的复制和解构赋值(let a = [2,3,4];let b = [...a])
升级部分
ES6在Array原型上新增了find()方法,用于取代传统的只能用indexOf查找包含数组项目的方法,且修复了indexOf查找不到NaN的bug([NaN].indexOf(NaN) === -1).此外还新增了copyWithin(),includes(),fill(),flat()等方法,可方便的用于字符串的查找,补全,转换等
优化部分
ES6在
Number原型上新增了isFinite(), isInteger(),isNaN()方法,用来取代传统的全局isFinite(),isNaN()方法检测数值是否有限、是否是NaN。ES5的isFinite(),isNaN()方法都会先将非数值类型的参数转化为Number类型再做判断,这其实是不合理的,最造成isNaN('NaN') === true的奇怪行为--'NaN'是一个字符串,但是isNaN却说这就是NaN。而Number.isFinite()和Number.isNaN()则不会有此类问题(Number.isNaN('NaN') === false)。(isFinite()同上)
升级部分
ES6在Math对象上新增了Math.cbrt(),trunc(),hypot()等等较多的科学计数法运算方法,可以更加全面的进行立方根、求和立方根等等科学计算
let a = 5;
//isFinite 判断是不是数字,如果是数字,就返回true,不是就返回false。
console.log(Number.isFinite(a)) // 结果 true
let bb = 452;
// isInteger 判断是不是整数,如果是整数就返回true,不是就false。
console.log(Number.isInteger(bb));
console.log(Number.isNaN('number'))
//主要用于检测是不是NaN,如果是NaN就返回true,不是就返回false。
//如果 x 是特殊的非数字值 NaN(或者能被转换为这样的值),返回的值就是 true。如果 x 是其他值,则返回 false。
// isNaN() 函数可用于判断其参数是否是 NaN,该值表示一个非法的数字(比如被 0 除后得到的结果)。
// 如果把 NaN 与任何值(包括其自身)相比得到的结果均是 false,所以要判断某个值是否是 NaN,不能使用 == 或 === 运算符。正因为如此,isNaN() 函数是必需的。
优化部分
对象属性变量式声明。
ES6可以直接以变量形式声明对象属性或者方法,。比传统的键值对形式声明更加简洁,更加方便,语义更加清晰
let [apple, orange] = ['red appe', 'yellow orange']
let myFruits = {
apple,
orange,
}
// let myFruits = {apple: 'red appe', orange: 'yellow orange'};
尤其在对象解构赋值(见优化部分b.)或者模块输出变量时,这种写法的好处体现的最为明显
let { keys, values, entries } = Object
let MyOwnMethods = {
keys,
values,
entries,
}
// let MyOwnMethods = {keys: keys, values: values, entries: entries}
可以看到属性变量式声明属性看起来更加简洁明了。方法也可以采用简洁写法
let es5Fun = {
method: function () {},
}
let es6Fun = {
method() {},
}
对象的解构赋值。 ES6 对象也可以像数组解构赋值那样,进行变量的解构赋值
let { apple, orange } = {
apple: 'red appe',
orange: 'yellow orange',
}
对象的扩展运算符(... )。 ES6对象的扩展运算符和数组扩展运算符用法本质上差别不大,毕竟数组也就是特殊的对象。对象的扩展运算符一个最常用也最好用的用处就在于可以轻松的取出一个目标对象内部全部或者部分的可遍历属性,从而进行对象的合并和分解
let { apple, orange, ...otherFruits } = {
apple: 'red apple',
orange: 'yellow orange',
grape: 'purple grape',
peach: 'sweet peach',
}
// otherFruits {grape: 'purple grape', peach: 'sweet peach'}
// 注意: 对象的扩展运算符用在解构赋值时,扩展运算符只能用在最有一个参数(otherFruits后面不能再跟其他参数)
let moreFruits = {
watermelon: 'nice watermelon',
}
let allFruits = {
apple,
orange,
...otherFruits,
...moreFruits,
}
super 关键字。ES6 在Class 类里新增了类似this 的关键字super 。同this 总是指向当前函数所在的对象不同,super 关键字总是指向当前函数所在对象的原型对象
升级部分
ES6在Object原型上新增了is()方法,做两个目标对象的相等比较,用来完善'==='方法。'==='方法中NaN === NaN //false其实是不合理的,Object.is修复了这个小bug。(Object.is(NaN, NaN) // true)
ES6在Object原型上新增了assign()方法,用于对象新增属性或者多个对象合并
const target = {
a: 1,
}
const source1 = {
b: 2,
}
const source2 = {
c: 3,
}
Object.assign(target, source1, source2)
target // {a:1, b:2, c:3}
注意 :
assign合并的对象target只能合并source1、source2中的自身属性,并不会合并source1、source2中的继承属性,也不会合并不可枚举的属性,且无法正确复制get和set属性(会直接执行get/set函数,取return的值)
-
ES6在Object原型上新增了getOwnPropertyDescriptors()方法,此方法增强了ES5中getOwnPropertyDescriptor()方法,可以获取指定对象所有自身属性的描述对象。结合defineProperties()方法,可以完美复制对象,包括复制get和set属性 -
ES6在Object原型上新增了getPrototypeOf()和setPrototypeOf()方法,用来获取或设置当前对象的prototype对象。这个方法存在的意义在于,ES5中获取设置prototype对像是通过__proto__属性来实现的,然而__proto__属性并不是ES规范中的明文规定的属性,只是浏览器各大产商“私自”加上去的属性,只不过因为适用范围广而被默认使用了,再非浏览器环境中并不一定就可以使用,所以为了稳妥起见,获取或设置当前对象的prototype对象时,都应该采用ES6新增的标准用法 -
ES6在Object原型上还新增了Object.keys(),Object.values(),Object.entries()方法,用来获取对象的所有键、所有值和所有键值对数组
// Object.keys
// 处理对象,返回可枚举的属性数组
let person={
name:'一只流浪的kk',
age:20,
eat:function(){}
}
console.log(Object.keys(person));// ['name','age','eat']
// 处理数组,返回索引值数组
let arr=[1,2,3,4,5];
console.log(Object.keys(arr));//['0','1','2','3','4','5']
// 处理字符串,返回索引值数组
let str='hello';
console.log(Object.keys(str));//['0','1','2','3','4']
// Object.values()
//返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值
let obj = {
foo : "bar",
baz : 20
};
console.log(Object.values(obj)); // ["bar", 20]
// 返回数组的成员顺序,与属性的遍历部分介绍的排列规则一致
const obj = {100 : "a", 2 : "b", 7 : "c"};
console.log(Object.values(obj)); //["b", "c", "a"]
// Object.values()只会遍历对象自身的可遍历属性
const obj = Object.create({},{p : {value : 10}});
console.log(Object.values(obj));
console.log(Object.getOwnPropertyDescriptors(obj)); // []
Object.create方法的第二个参数添加的对象属性(属性p),如果不显式声明,默认是不可遍历的,因为p的属性描述对象的enumerable默认是false,Object.values不会返回这个属性。
因此只要把enumerable改成true,Object.values就会返回属性p的值。
const obj = Object.create({},{p:{ value : 10, enumerable : true, configurable : true, writable : true, }}) console.log(Object.values(obj)); //[10]
//Object.values会过滤属性名为 Symbol 值的属性
//如果Object.values方法的参数是一个字符串,会返回各个字符组成的一个数组。
Object.values({ [Symbol()]: 123, foo: 'abc' });
console.log(Object.values('foo')); //["f", "o", "o"]
// 如果参数不是对象,Object.values会先将其转为对象
console.log(Object.values(42)); // []
console.log(Object.values(true)); // []
console.log(Object.values(undefined)); //error
console.log(Object.values(null)); //error
Object.entries()
var obj = { foo: 'bar', baz: 42 };
console.log(Object.entries(obj));
//如果原对象的属性名是一个 Symbol 值,该属性会被省略
console.log(Object.entries({ [Symbol()]: 123, foo: 'abc' })); // [ [ 'foo', 'abc' ] ]
// 遍历对象的属性
let obj = {
one : 1,
two : 2,
}
for(let [k , v] of Object.entries(obj)){
console.log(`${JSON.stringify(k)} : ${JSON.stringify(v)}`);
}
// 将对象转为真正的Map结构
const obj = {foo : "bar", baz : 10};
const map = new Map(Object.entries(obj));
console.log(map);
// 实现Object.entries方法
const entries = (obj) => {
let result = [];
const objType = typeof(obj);
if(obj === undefined || obj === null){
throw new TypeError();
}
if(objType === "number" || objType === "boolean"){
return [];
}
for(let k of Object.keys(obj)){
result.push([k,obj[k]]);
}
return result
}
优化部分
箭头函数(核心)。箭头函数是ES6核心的升级项之一,箭头函数里没有自己的this,这改变了以往JS函数中最让人难以理解的this运行机制。主要优化点
- 箭头函数内的this指向的是函数定义时所在的对象,而不是函数执行时所在的对象。ES5函数里的this总是指向函数执行时所在的对象,这使得在很多情况下
this的指向变得很难理解,尤其是非严格模式情况下,this有时候会指向全局对象,这甚至也可以归结为语言层面的bug之一。ES6的箭头函数优化了这一点,它的内部没有自己的this,这也就导致了this总是指向上一层的this,如果上一层还是箭头函数,则继续向上指,直到指向到有自己this的函数为止,并作为自己的this - 箭头函数不能用作构造函数,因为它没有自己的
this,无法实例化 - 也是因为箭头函数没有自己的this,所以箭头函数 内也不存在
arguments对象。(可以用扩展运算符代替) - 函数默认赋值。
ES6之前,函数的形参是无法给默认值得,只能在函数内部通过变通方法实现。ES6以更简洁更明确的方式进行函数默认赋值
function es6Fuc(x, y = 'default') {
console.log(x, y)
}
es6Fuc(4)
// 4, default
升级部分
ES6新增了双冒号运算符,用来取代以往的
bind,call,和apply。(浏览器暂不支持,Babel已经支持转码)
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
return obj::hasOwnProperty(key);
}
箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。但是,箭头函数并不适用于所有场合,所以现在有一个提案,提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。
函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。
如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
let log = ::console.log;
// 等同于
var log = console.log.bind(console);
Symbol是ES6引入的第七种原始数据类型(说法不准确,应该是第七种数据类型,Object不是原始数据类型之一,已更正),所有Symbol()生成的值都是独一无二的,可以从根本上解决对象属性太多导致属性名冲突覆盖的问题。对象中Symbol()属性不能被for...in遍历,但是也不是私有属性
Set是ES6引入的一种类似Array的新的数据结构,Set实例的成员类似于数组item成员,区别是Set实例的成员都是唯一,不重复的。这个特性可以轻松地实现数组去重
Map是ES6引入的一种类似Object的新的数据结构,Map可以理解为是Object的超集,打破了以传统键值对形式定义对象,对象的key不再局限于字符串,也可以是Object。可以更加全面的描述对象的属性
Proxy是ES6新增的一个构造函数,可以理解为JS语言的一个代理,用来改变JS默认的一些语言行为,包括拦截默认的get/set等底层方法,使得JS的使用自由度更高,可以最大限度的满足开发者的需求。比如通过拦截对象的get/set方法,可以轻松地定制自己想要的key或者value。下面的例子可以看到,随便定义一个myOwnObj的key,都可以变成自己想要的函数`
function createMyOwnObj() {
//想把所有的key都变成函数,或者Promise,或者anything
return new Proxy(
{},
{
get(target, propKey, receiver) {
return new Promise((resolve, reject) => {
setTimeout(() => {
let randomBoolean = Math.random() > 0.5
let Message
if (randomBoolean) {
Message = `
你的
${propKey}
运气不错,成功了
`
resolve(Message)
} else {
Message = `
你的
${propKey}
运气不行,失败了
`
reject(Message)
}
}, 1000)
})
},
}
)
}
let myOwnObj = createMyOwnObj()
myOwnObj.hahaha
.then((result) => {
console.log(result)
//你的hahaha运气不错,成功了
})
.catch((error) => {
console.log(error)
//你的hahaha运气不行,失败了
})
myOwnObj.wuwuwu
.then((result) => {
console.log(result)
//你的wuwuwu运气不错,成功了
})
.catch((error) => {
console.log(error)
//你的wuwuwu运气不行,失败了
})
Proxy属性操作介绍
- get(target, propKey, receiver) :拦截对象属性的读取
- set(target, propKey, value, receiver) :拦截对象属性的设置返回一个布尔值。
- has(target, propKey) :拦截propKey in proxy的操作,返回一个布尔值。
- deleteProperty(target, propKey) :拦截delete proxy[propKey]的操作,返回一个布尔值。
- ownKeys(target) :拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
- getOwnPropertyDescriptor(target, propKey) :拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
- defineProperty(target, propKey, propDesc) :拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
- preventExtensions(target) :拦截Object.preventExtensions(proxy),返回一个布尔值。
- getPrototypeOf(target) :拦截Object.getPrototypeOf(proxy),返回一个对象。
- isExtensible(target) :拦截Object.isExtensible(proxy),返回一个布尔值。
- setPrototypeOf(target, proto) :拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
- apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
- construct(target, args) :拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
一些常用的Proxy属性用法示例
get(target, prop, receiver)
-
作用:拦截对目标对象属性的读取操作。
-
参数:
- target:目标对象。
- prop:要访问的属性名。
- receiver:代理对象本身(或者说是拦截操作所在的环境)。
-
示例:
const target = {
name: 'Alice',
age: 25
};
const proxy = new Proxy(target, {
get(target, prop) {
console.log(`Getting ${prop}`);
return target[prop];
}
});
console.log(proxy.name); // 输出:Getting name,Alice
set(target, prop, value, receiver)
-
作用:拦截对目标对象属性的赋值操作。
-
参数:
- target:目标对象。
- prop:要设置的属性名。
- value:要设置的值。
- receiver:代理对象本身(或者说是拦截操作所在的环境)。
-
示例:
const target = {
name: 'Alice',
age: 25
};
const proxy = new Proxy(target, {
set(target, prop, value) {
console.log(`Setting ${prop} to ${value}`);
target[prop] = value;
return true;
}
});
proxy.age = 30; // 输出:Setting age to 30
console.log(proxy.age); // 输出:30
has(target, prop)
-
作用:拦截对in操作符的操作,判断属性是否存在。
-
参数:
- target:目标对象。
- prop:要判断的属性名。
-
示例:
const target = {
name: 'Alice',
age: 25
};
const proxy = new Proxy(target, {
has(target, prop) {
console.log(`Checking if ${prop} exists`);
return prop in target;
}
});
console.log('name' in proxy); // 输出:Checking if name exists,true
console.log('gender' in proxy); // 输出:Checking if gender exists,false
deleteProperty(target, prop)
-
作用:拦截对delete操作符的操作,删除属性。
-
参数:
- target:目标对象。
- prop:要删除的属性名。
-
示例:
const target = {
name: 'Alice',
age: 25
};
const proxy = new Proxy(target, {
deleteProperty(target, prop) {
console.log(`Deleting ${prop}`);
delete target[prop];
return true;
}
});
delete proxy.age; // 输出:Deleting age console.log(proxy.age); // 输出:undefined
apply(target, thisArg, argumentsList)
-
作用:拦截对代理对象的函数调用。
-
参数:
- target:目标对象。
- thisArg:函数调用时的this值。
- argumentsList:函数调用时的参数列表。
-
示例:
const target = function (name) {
console.log(`Hello, ${name}`);
};
const proxy = new Proxy(target, {
apply(target, thisArg, argumentsList) {
console.log('Calling function');
return target.apply(thisArg, argumentsList);
}
});
proxy('Alice'); // 输出:Calling function,Hello, Alice
construct(target, argumentsList, newTarget)
-
作用:拦截对代理对象的构造函数调用。
-
参数:
- target:目标对象。
- argumentsList:构造函数调用时的参数列表。
- newTarget:最初被调用的构造函数。
-
示例:
const target = function (name) {
this.name = name;
};
const proxy = new Proxy(target, {
construct(target, argumentsList) {
console.log('Constructing object');
return new target(...argumentsList);
}
});
const obj = new proxy('Alice'); // 输出:Constructing object
console.log(obj.name); // 输出:Alice
getPrototypeOf(target)
-
作用:拦截对代理对象原型链上属性访问操作。
-
参数:
- target:目标对象。
-
示例:
const target = {};
const proxy = new Proxy(target, {
getPrototypeOf(target) {
console.log('Getting prototype');
return Object.getPrototypeOf(target);
}
});
console.log(Object.getPrototypeOf(proxy)); // 输出:Getting prototype,{}
setPrototypeOf(target, prototype)
- 作用:拦截对代理对象原型链上属性设置操作。
- 参数:
- target:目标对象。
- prototype:要设置的原型对象。
- 示例:
const target = {}
const proxy = new Proxy(target, {
setPrototypeOf(target, prototype) {
console.log('Setting prototype')
return Object.setPrototypeOf(target, prototype)
}
})
const proto = { name: 'Alice' }
Object.setPrototypeOf(proxy, proto) // 输出:Setting prototype
console.log(proxy.name) // 输出:Alice
Proxy是ES6中的一个重要特性,它提供了一种拦截和修改对象默认操作的能力。通过使用Proxy,我们可以实现访问控制、数据验证、属性劫持等功能。Proxy的原理是通过在代理对象上定义一组钩子函数来实现拦截和自定义处理。在实际应用中,我们可以根据需要使用Proxy来解决各种问题,提高代码的灵活性和可维护性。
Reflect是ES6引入的一个新的对象,他的主要作用有两点,一是将原生的一些零散分布在Object、Function或者全局函数里的方法(如apply、delete、get、set等等),统一整合到Reflect上,这样可以更加方便更加统一的管理一些原生API。其次就是因为Proxy可以改写默认的原生API,如果一旦原生API别改写可能就找不到了,所以Reflect也可以起到备份原生API的作用,使得即使原生API被改写了之后,也可以在被改写之后的API用上默认的API
Promise是ES6引入的一个新的对象,他的主要作用是用来解决JS异步机制里,回调机制产生的“回调地狱”。它并不是什么突破性的API,只是封装了异步回调形式,使得异步回调可以写的更加优雅,可读性更高,而且可以链式调用
-
Iterator是ES6中一个很重要概念,它并不是对象,也不是任何一种数据类型。因为ES6新增了Set、Map类型,他们和Array、Object类型很像,Array、Object都是可以遍历的,但是Set、Map都不能用for循环遍历,解决这个问题有两种方案,一种是为Set、Map单独新增一个用来遍历的API,另一种是为Set、Map、Array、Object新增一个统一的遍历API,显然,第二种更好,ES6也就顺其自然的需要一种设计标准,来统一所有可遍历类型的遍历方式。Iterator正是这样一种标准。或者说是一种规范理念 - 就好像
JavaScript是ECMAScript标准的一种具体实现一样,Iterator标准的具体实现是Iterator遍历器。Iterator标准规定,所有部署了key值为[Symbol.iterator],且[Symbol.iterator]的value是标准的Iterator接口函数(标准的Iterator接口函数: 该函数必须返回一个对象,且对象中包含next方法,且执行next()能返回包含value/done属性的Iterator对象)的对象,都称之为可遍历对象,next()后返回的Iterator对象也就是Iterator遍历器
//obj就是可遍历的,因为它遵循了Iterator标准,且包含[Symbol.iterator]方法,方法函数也符合标准的Iterator接口规范。
//obj.[Symbol.iterator]() 就是Iterator遍历器
let obj = {
data: ['hello', 'world'],
[Symbol.iterator]() {
const self = this
let index = 0
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
}
} else {
return {
value: undefined,
done: true
}
}
},
}
},
}
ES6给Set、Map、Array、String都加上了[Symbol.iterator]方法,且[Symbol.iterator]方法函数也符合标准的Iterator接口规范,所以Set、Map、Array、String默认都是可以遍历的
//Array
let array = ['red', 'green', 'blue']
array[Symbol.iterator]()
//Iterator遍历器
array[Symbol.iterator]().next()
//{value: "red ", done: false}
//String
let string = '1122334455'
string[Symbol.iterator]()
//Iterator遍历器
string[Symbol.iterator]().next()
//{value: "1 ", done: false}
//set
let set = new Set(['red', 'green', 'blue'])
set[Symbol.iterator]()
//Iterator遍历器
set[Symbol.iterator]().next()
//{value: "red ", done: false}
//Map
let map = new Map()
let obj = {
map: 'map',
}
map.set(obj, 'mapValue')
map[Symbol.iterator]().next()
// {
// value: Array(2),
// done: false
// }
所有部署了载了
Iterator接口的对象(可遍历对象)都可以通过for...of去遍历,而for..in仅仅可以遍历对象
- 这也就意味着,数组也可以用
for...of遍历,这极大地方便了数组的取值,且避免了很多程序用for..in去遍历数组的恶习
- 如果说
JavaScript是ECMAScript标准的一种具体实现、Iterator遍历器是Iterator的具体实现,那么Generator函数可以说是Iterator接口的具体实现方式。 - 执行
Generator函数会返回一个遍历器对象,每一次Generator函数里面的yield都相当一次遍历器对象的next()方法,并且可以通过next(value)方法传入自定义的value,来改变Generator函数的行为。 -
Generator函数可以通过配合Thunk函数更轻松更优雅的实现异步编程和控制流管理。
-
Generator是ES6中新增的语法,和Promise一样,都可以用来异步编程
// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
let a = 1 + 2
yield 2
yield 3
}
let b = test()
console.log(b.next())
// >{ value: 2, done: false }
console.log(b.next())
// >{ value: 3, done: false }
console.log(b.next())
// >{ value: undefined, done: true }
从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现
// cb 也就是编译过的 test 函数
function generator(cb) {
return (function () {
var object = {
next: 0,
stop: function () {},
}
return {
next: function () {
var ret = cb(object)
if (ret === undefined)
return {
value: undefined,
done: true
}
return{
value: ret,
done: false
}
},
}
})()
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
var a
return generator(function (_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 可以发现通过 yield 将代码分割成几块
// 每次执行 next 函数就执行一块代码
// 并且表明下次需要执行哪块代码
case 0:
a = 1 + 2
_context.next = 4
return 2
case 4:
_context.next = 6
return
3
// 执行完毕
case 6:
case 'end ':
return _context.stop()
}
}
})
}
-
Generator是ES6中新增的语法,和Promise一样,都可以用来异步编程
// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
let a = 1 + 2
yield 2
yield 3
}
let b = test()
console.log(b.next())
// >{ value: 2, done: false }
console.log(b.next())
// >{ value: 3, done: false }
console.log(b.next())
// >{ value: undefined, done: true }
从以上代码可以发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现
// cb 也就是编译过的 test 函数
function generator(cb) {
return (function () {
var object = {
next: 0,
stop: function () {},
}
return {
next: function () {
var ret = cb(object)
if (ret === undefined)
return {
value: undefined,
done: true,
}
return {
value: ret,
done: false,
}
},
}
})()
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
var a
return generator(function (_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 可以发现通过 yield 将代码分割成几块
// 每次执行 next 函数就执行一块代码
// 并且表明下次需要执行哪块代码
case 0:
a = 1 + 2
_context.next = 4
return 2
case 4:
_context.next = 6
return 3
// 执行完毕
case 6:
case 'end ':
return _context.stop()
}
}
})
}
async函数可以理解为内置自动执行器的Generator函数语法糖,它配合ES6的Promise近乎完美的实现了异步编程解决方案
async、await 优缺点
async和await相比直接使用Promise来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码。缺点在于滥用await可能会导致性能问题,因为await会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性
下面来看一个使用 await 的代码。
var a = 0
var b = async () => {
a = a + (await 10)
console.log('2', a)
// ->'2' 10
a = (await 10) + a
console.log('3', a)
// ->'3' 20
}
b()
a++
console.log('1', a)
// ->'1' 1
- 首先函数
b先执行,在执行到await 10之前变量a还是0,因为在await内部实现了generators,generators会保留堆栈中东西,所以这时候a = 0被保存了下来 - 因为
await是异步操作,遇到await就会立即返回一个pending状态的Promise对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行console.log('1', a) - 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候
a = 10 - 然后后面就是常规执行代码了
-
19 Class、extends是什么,有什么作用?
-
ES6的class可以看作只是一个ES5生成实例对象的构造函数的语法糖。它参考了java语言,定义了一个类的概念,让对象原型写法更加清晰,对象实例化更像是一种面向对象编程。Class类可以通过extends实现继承。它和ES5构造函数的不同点类的内部定义的所有方法,都是不可枚举的
///ES5
function ES5Fun(x, y) {
this.x = x
this.y = y
}
ES5Fun.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')'
}
var p = new ES5Fun(1, 3)
p.toString()
Object.keys(ES5Fun.prototype)
//['toString']
//ES6
class ES6Fun {
constructor(x, y) {
this.x = x
this.y = y
}
toString() {
return '(' + this.x + ', ' + this.y + ')'
}
}
Object.keys(ES6Fun.prototype)
//[]
-
ES6的class类必须用new命令操作,而ES5的构造函数不用new也可以执行。 -
ES6的class类不存在变量提升,必须先定义class之后才能实例化,不像ES5中可以将构造函数写在实例化之后。 -
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面。ES6的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
module、export、import是ES6用来统一前端模块化方案的设计思路和实现方案。export、import的出现统一了前端模块化的实现方案,整合规范了浏览器/服务端的模块化方法,用来取代传统的AMD/CMD、requireJS、seaJS、commondJS等等一系列前端模块不同的实现方案,使前端模块化更加统一规范,JS也能更加能实现大型的应用程序开发。import引入的模块是静态加载(编译阶段加载)而不是动态加载(运行时加载)。import引入export导出的接口值是动态绑定关系,即通过该接口,可以取到模块内部实时的值
- 常用箭头函数来取代
var self = this;的做法。- 常用
let取代var命令。- 常用数组/对象的结构赋值来命名变量,结构更清晰,语义更明确,可读性更好。
- 在长字符串多变量组合场合,用模板字符串来取代字符串累加,能取得更好地效果和阅读体验。
- 用
Class类取代传统的构造函数,来生成实例化对象。- 在大型应用开发中,要保持
module模块化开发思维,分清模块之间的关系,常用import、export方法。
新增模板字符串(为JavaScript提供了简单的字符串插值功能)、箭头函数(操作符左边为输入的参数,而右边则是进行的操作以及返回的值Inputs=>outputs。)、for-of(用来遍历数据—例如数组中的值。)arguments对象可被不定参数和默认参数完美代替。ES6将promise对象纳入规范,提供了原生的Promise对象。增加了let和const命令,用来声明变量。增加了块级作用域。let命令实际上就增加了块级作用域。ES6规定,var命令和function命令声明的全局变量,属于全局对象的属性;let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。还有就是引入module模块的概念
- 依照
Promise/A+的定义,Promise 有四种状态:
pending: 初始状态, 非fulfilled或rejected.fulfilled: 成功的操作.rejected: 失败的操作.settled:Promise已被fulfilled或rejected,且不是pending- 另外,
fulfilled与rejected一起合称 settledPromise对象用来进行延迟(deferred) 和异步(asynchronous) 计算- 可以把
Promise看成一个状态机。初始是pending状态,可以通过函数resolve和reject,将状态转变为resolved或者rejected状态,状态一旦改变就不能再次变化。then函数会返回一个Promise实例,并且该返回值是一个新的实例而不是之前的实例。因为Promise规范规定除了pending状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个then调用就失去意义了
构造一个 Promise ,最基本的用法如下:
// 创建一个新的 Promise 对象
let myPromise = new Promise((resolve, reject) => {
// 异步操作的代码
let success = true; // 假设这是操作的成功标志
if (success) {
// 如果操作成功,调用 resolve 并传入结果
resolve('操作成功完成');
} else {
// 如果操作失败,调用 reject 并传入错误信息
reject(new Error('操作失败'));
}
});
// 使用 then 方法来处理 Promise 的成功情况
// 使用 catch 方法来处理 Promise 的失败情况
myPromise.then((result) => {
console.log(result); // 输出: 操作成功完成
}).catch((error) => {
console.error(error.message); // 输出: 操作失败
});
什么是 Promise ?
- Promise 就是一个对象,用来表示并传递异步操作的最终结果
- Promise 最主要的交互方式:将回调函数传入 then 方法来获得最终结果或出错原因
- Promise 代码书写上的表现:以“链式调用”代替回调函数层层嵌套(回调地狱)
- 块级作用区域
let a = 1;- 可定义常量
const PI = 3.141592654;- 变量解构赋值
var [a, b, c] = [1, 2, 3];- 字符串的扩展(模板字符串)
var sum =${a + b};- 数组的扩展(转换数组类型)
Array.from($('li'));- 函数的扩展(扩展运算符)
[1, 2].push(...[3, 4, 5]);- 对象的扩展(同值相等算法)
Object.is(NaN, NaN);- 新增数据类型(Symbol)
let uid = Symbol('uid');- 新增数据结构(Map)
let set = new Set([1, 2, 2, 3]);- for...of循环
for(let val of arr){};- Promise对象
var promise = new Promise(func);- Generator函数
function* foo(x){yield x; return x*x;}- 引入Class(类)
class Foo {}- 引入模块体系
export default func;- 引入async函数[ES7]
async function asyncPrint(value, ms) { await timeout(ms) console.log(value) }
==相等运算符,比较时会自动进行数据类型转换===严格相等运算符,比较时不进行隐式类型转换Object.is同值相等算法,在===基础上对0和NaN特别处理
;+0 === -0
//true
NaN === NaN
// false
Object.is(+0, -0)
// false
Object.is(NaN, NaN)
// true
Babel是一个 JS 编译器,自带一组 ES6 语法转化器,用于转化 JS 代码。 这些转化器让开发者提前使用最新的 JS语法(ES6/ES7),而不用等浏览器全部兼容。Babel默认只转换新的 JS 句法(syntax),而不转换新的API。
可以用来表示一个独一无二的变量防止命名冲突。但是面试官问还有吗?我没想出其他的用处就直接答我不知道了,还可以利用 symbol 不会被常规的方法(除了 Object.getOwnPropertySymbols 外)遍历到,所以可以用来模拟私有变量。
主要用来提供遍历接口,布置了 symbol.iterator 的对象才可以使用 for···of 循环,可以统一处理数据结构。调用之后回返回一个遍历器对象,包含有一个 next 方法,使用 next 方法后有两个返回值 value 和 done 分别表示函数当前执行位置的值和是否遍历完毕。
Symbol.for()可以在全局访问symbol
在有 Babel 的情况下,我们可以直接使用 ES6 的模块化
// file a.js
export function a() {}
export function b() {}
// file b.js
export default function () {}
import { a, b } from './a.js'
import XXX from './b.js'
CommonJS
CommonJs是Node独有的规范,浏览器中使用就需要用到Browserify解析了。
// a.js
module.exports = {
a: 1,
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // ->log 1
在上述代码中,module.exports 和 exports 很容易混淆,让我们来看看大致内部实现
var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1,
}
// 基本实现
var module = {
exports: {},
// exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
}
再来说说
module.exports和exports,用法其实是相似的,但是不能对exports直接赋值,不会有任何效果。
对于
CommonJS和ES6中的模块化的两者区别是:
- 前者支持动态导入,也就是
require(${path}/xx.js),后者目前不支持,但是已有提案,前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。 - 而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
- 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。
- 但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
- 后者会编译成
require/exports来执行的
AMD
AMD是由RequireJS提出的
// AMD
define(['./a', './b'], function (a, b) {
a.do()
b.do()
})
define(function (require, exports, module) {
var a = require('./a')
a.doSomething()
var b = require('./b')
b.doSomething()
})
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
箭头函数其实是没有
this的,这个函数中的this只取决于他外面的第一个不是箭头函数的函数的this。在这个例子中,因为调用a符合前面代码中的第一个情况,所以this是window。并且this一旦绑定了上下文,就不会被任何代码改变
普通函数、箭头函数的区别
- 箭头函数不绑定
arguments,可以使用...args代替 - 箭头函数没有
prototype属性,不能进行new实例化,亦不能通过call、apply等绑定this - 箭头函数的
this指向创建时父级的this
- ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.apply(this))
- ES6 的继承机制完全不同,实质上是先创建父类的实例对象 this(所以必须先调用父类的 super()方法),然后再用子类的构造函数修改 this。
- ES5 的继承时通过原型或构造函数机制来实现。
- ES6 通过 class 关键字定义类,里面有构造方法,类之间通过 extends 关键字实现继承。
- 子类必须在 constructor 方法中调用 super 方法,否则新建实例报错。因为子类没有自己的 this 对象,而是继承了父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类得不到 this 对象。
- 注意 super 关键字指代父类的实例,即父类的 this 对象。
- 注意:在子类构造函数中,调用 super 后,才可使用 this 关键字,否则报错。function 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量
// 在 ES5 中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。
var a = 12
function f() {}
console.log(window.a)
// 12console.log(window.f);
// f(){}
// 但 ES6 规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。
let aa = 1
const bb = 2
console.log(window.aa)
// undefined console.log(window.bb);
// undefined
// 在哪里?怎么获取?通过在设置断点,看看浏览器是怎么处理的:
//通过上图也可以看到,在全局作用域中,用 let 和 const 声明的全局变量并没
//有在全局对象中,只是一个块级作用域(Script)中怎么获取?在定义变量的块级作用域中就能获取啊,既然不属于顶层对象,那就不加 window(global)呗。
let aa = 1
const bb = 2
console.log(aa)
// 1console.log(bb);
// 2
Set
- 成员不能重复
- 只有健值,没有健名,有点类似数组
- 可以遍历,方法有
add,delete,has
weakSet
- 成员都是对象
- 成员都是弱引用,随时可以消失。 可以用来保存DOM节点,不容易造成内存泄漏
- 不能遍历,方法有
add,delete,has
Map
- 本质上是健值对的集合,类似集合
- 可以遍历,方法很多,可以干跟各种数据格式转换
weakMap
- 直接受对象作为健名(
null除外),不接受其他类型的值作为健名 - 健名所指向的对象,不计入垃圾回收机制
- 不能遍历,方法同
get,set,has,delete
-
34 Promise.all() 和 Promise.allSettled()的比较
- 接受的结果与入参时的promise实例一一对应,且结果的每一项都是一个对象,告诉你结果和值,对象内都有一个属性叫“status”,用来明确知道对应的这个promise实例的状态(fulfilled或rejected),fulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值。
const resolved = Promise.resolve(42)
const rejected = Promise.reject(-1)
const allSettledPromise = Promise.allSettled([resolved, rejected])
allSettledPromise.then(function (results) {
console.log(results)
})
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]
重要的一点是,他不论接受入参的promise本身的状态,会返回所有promise的结果,但这一点Promise.all 做不到,如果你需要知道所有入参的异步操作的所有结果,或者需要知道这些异步操作是否全部结束,应该使用promise.allSettled()
- 只有当所有入参的promise实例都是
fulfilled状态,才会在Promise.all().then()方法中结果,返回结果也是与入参一一对应,结果中只包含实际的resolve的结果,不包含类似allSettled的status和value属性。
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve('Promise 1 finished'), 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve('Promise 2 finished'), 2000);
});
Promise.all([promise1, promise2])
.then(results => {
console.log(results); // 输出: ["Promise 1 finished", "Promise 2 finished"]
// 所有异步请求都已完成,可以进行后续操作
})
.catch(error => {
console.error(error);
// 如果有任何一个 Promise 被 rejected,则会在这里捕获到错误
});