本章介绍的内容并不局限在es2015,可能es2015-es2020都有涉及,后续也会更新一些
let和const
先来一道经典面试题
for(var i=0;i<=3;i++){
setTimeout(function() {
console.log(i)
}, 10);
}
//运行这段代码会输出什么内容?为什么?
//4 4 4 4
原因:
1、var定义的变量是全局的, 所以全局只有一个变量i.
2、setTimeout是异步, 在下一轮事件循环执行, 等到执行的时候, 去找i变量的引用。
所以函数找到了遍历完后的i, 此时它已经变成了4。
//想要输出 0 1 2 3
for(let i=0;i<=3;i++){
setTimeout(function() {
console.log(i)
}, 10);
}
//运行这段代码会输出什么内容?为什么?
//0 1 2 3
原因:
1、let引入了块级作用域的概念, 创建setTimeout函数时,变量i在作用域内生效。
对于循环的每个迭代,引用的i都是不同的。
//不使用let如何实现
for(let i=0;i<=3;i++){
(function(i){
setTimeout(function() {
console.log(i)
}, 10);
})(i)
}
上面的问题主要是体现的是let的块级作用域,除了块级作用域,let和var还有另一个区别就是变量提升的问题
console.log(i) // undefinde
var i = 1;
console.log(letI) //letI is not defined 报错
let letI = 2;
const有let的特点,并且多了一个特性,const声明的基本类型变量不会被改变
箭头函数
1、this指向问题
1.普通函数的this:指向它的调用者(使用函数时决定this指向),如果没有调用者则默认指向window.
2.箭头函数的this: 指向箭头函数定义时所处的对象,默认使用父级的this(window).
区别:箭头函数里的this是定义的时候决定的, 普通函数里的this是使用的时候决定的。
2、简写方式
const arrowFn = (value) => Number(value);
const arrowFn = function(value){
return Number(value)
}
3、箭头函数不能被用作构造函数
构造函数:改变this指向到新实例出来的对象.
箭头函数:this指向是定义的时候决定的.
class
class Test{
_name=''
//这个类需要实例的时候
constructor(name){
this.name = name
}
//静态属性
static getForName(){
return `${this.name}----`
}
get name(){
return this._name
}
set name(val){
console.log('修改name')
this._name = val
}
}
let init = new Test('test')
console.log(init.name) // 修改name test
模板字符串
识别换行
const c = `我是换行
我换行了!
我又换行了!
`;
面试题来一道. 编写render函数, 实现模板字符串功能.
const year = '2021';
const month = '10';
const day = '01';
let template = '${year}-${month}-${day}';
let context = { year, month, day };
const str = render(template)(context);
console.log(str) // 2021-10-01
function render(template) {
return function(context) {
return template.replace(/\$\{(.*?)\}/g, (match, key) => context[key]);
}
}
解构
1、数组结构
// 基础类型解构
let [a, b, c] = [1, 2, 3]
console.log(a, b, c) // 1, 2, 3
// 对象数组解构
let [a, b, c] = [{name: '1'}, {name: '2'}, {name: '3'}]
console.log(a, b, c) // {name: '1'}, {name: '2'}, {name: '3'}
// ...解构
let [head, ...tail] = [1, 2, 3, 4]
console.log(head, tail) // 1, [2, 3, 4]
// 嵌套解构
let [a, [b], d] = [1, [2, 3], 4]
console.log(a, b, d) // 1, 2, 4
// 解构不成功为undefined
let [a, b, c] = [1]
console.log(a, b, c) // 1, undefined, undefined
// 解构默认赋值
let [a = 1, b = 2] = [3]
console.log(a, b) // 3, 2
2、对象的结构
// 对象属性解构
let { f1, f2 } = { f1: 'test1', f2: 'test2' }
console.log(f1, f2) // test1, test2
// 可以不按照顺序,这是数组解构和对象解构的区别之一
let { f2, f1 } = { f1: 'test1', f2: 'test2' }
console.log(f1, f2) // test1, test2
// 解构对象重命名
let { f1: rename, f2 } = { f1: 'test1', f2: 'test2' }
console.log(rename, f2) // test1, test2
// 嵌套解构
let { f1: {f11}} = { f1: { f11: 'test11', f12: 'test12' } }
console.log(f11) // test11
// 默认值
let { f1 = 'test1', f2: rename = 'test2' } = { f1: 'current1', f2: 'current2'}
console.log(f1, rename) // current1, current2
3、解构的原理是什么?
针对可迭代对象的Iterator接口,通过遍历器按顺序获取对应的值进行赋值.
3.1 Iterator 是什么?
Iterator是一种接口,为各种不一样的数据解构提供统一的访问机制。任何数据解构只要有Iterator接口,就能通过遍历操作,依次按顺序处理数据结构内所有成员。
3.2 Iterator有什么用?
- 为各种数据解构提供统一的访问接口
- 使得数据解构能按次序排列处理
- for of 遍历
生成一个Iterator
function generateIterator(array) {
let nextIndex = 0
return {
next: () => nextIndex < array.length ? {
value: array[nextIndex++],
done: false
} : {
value: undefined,
done: true
}
};
}
const iterator = generateIterator([0, 1, 2])
console.log(iterator.next()) {value:0,done: false}
console.log(iterator.next()) {value:1,done: false}
console.log(iterator.next()) {value:2,done: false}
console.log(iterator.next()) {value:undefined,done: true}
3.3 可迭代对象是什么?
可迭代对象是Iterator接口的实现。这是ECMAScript 2015的补充,它不是内置或语法,而仅仅是协议。任何遵循该协议点对象都能成为可迭代对象。可迭代对象得有两个协议:可迭代协议和迭代器协议。
-
可迭代协议:对象必须实现iterator方法。即对象或其原型链上必须有一个名叫Symbol.iterator的属性。该属性的值为无参函数,函数返回迭代器协议。
//Symbol.iterator:()=>迭代器协议 -
迭代器协议:必须实现一个next()方法,该方法返回对象有done(boolean)和value属性。
3.4 自己来实现一个可以for of遍历的对象?
通过以上可知,自定义数据结构,只要拥有Iterator接口,并将其部署到自己的Symbol.iterator属性上,就可以成为可迭代对象,能被for of循环遍历。
const obj = {
count: 0,
[Symbol.iterator]: () => {
return {
next: () => {
obj.count++;
if (obj.count <= 10) {
return {
value: obj.count,
done: false
}
} else {
return {
value: undefined,
done: true
}
}
}
}
}
}
for (const item of obj) {
console.log(item)
}
1,2,3,4,5,6,7,8,9,10
遍历
1、for in
遍历数组时,key为数组下标字符串;遍历对象,key为对象字段名。
let obj = {a: 'test1', b: 'test2'}
for (let key in obj) {
console.log(key, obj[key])
}
缺点::
for in 不仅会遍历当前对象,还包括原型链上的可枚举属性
for in 不适合遍历数组,主要应用为对象
2、for of
可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments对象,NodeList对象)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。
let arr = [{age: 1}, {age: 5}, {age: 100}, {age: 34}]
for(let {age} of arr) {
if (age > 10) {
break // for of 允许中断
}
console.log(age)
}
优点:
for of 仅遍历当前对象
Object
1、 Object.keys
该方法返回一个给定对象的自身可枚举属性组成的数组。
const obj = { a: 1, b: 2 };
const keys = Object.keys(obj); // [a, b]
手写实现一个函数模拟Object.keys?
function getObjectKeys(obj) {
const result = [];
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
result.push(prop);
}
}
return result;
}
console.log(getObjectKeys({
a: 1,
b: 2
}))
2、 Object.values
该方法返回一个给定对象自身的所有可枚举属性值的数组。
const obj = { a: 1, b: 2 };
const keys = Object.keys(obj); // [1, 2]
手写实现一个函数模拟Object.values?
function getObjectValues(obj) {
const result = [];
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
result.push(obj[prop]);
}
}
return result;
}
console.log(getObjectValues({
a: 1,
b: 2
}))
3、 Object.entries
该方法返回一个给定对象自身可枚举属性的键值对数组。
const obj = { a: 1, b: 2 };
const keys = Object.entries(obj); // [ [ 'a', 1 ], [ 'b', 2 ] ]
手写实现一个函数模拟Object.entries?
function getObjectEntries(obj) {
const result = [];
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
result.push([prop, obj[prop]]);
}
}
return result;
}
console.log(getObjectEntries({
a: 1,
b: 2
}))
4、Object.getOwnPropertyNames
该方法返回一个数组,该数组对元素是 obj自身拥有的枚举或不可枚举属性名称字符串。
看一下这段代码会输出什么?
Object.prototype.aa = '1111';
const testData = {
a: 1,
b: 2
}
for (const key in testData) {
console.log(key);
}
console.log(Object.getOwnPropertyNames(testData))
5、 Object.defineProperty
const object1 = {};
Object.defineProperty(object1, 'p1', {
value: 'lubai',
writable: false
});
object1.p1 = 'not lubai';
console.log(object1.p1);
讲到了defineProperty, 肯定离不开Proxy.
const obj = new Proxy({}, {
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}`);
return target[propKey];
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}`);
return Reflect.set(target, propKey, value, receiver);
}
});
obj.something = 1;
console.log(obj.something);
Reflect又是个什么东西?
将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法
让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
6、Object.assign
浅拷贝, 类似于 { ...a, ...b };
function shallowClone(source) {
const target = {};
for (const i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
}
const a = {
b: 1,
c: {
d: 111
}
}
const b = shallowClone(a);
b.b = 2222;
b.c.d = 333;
console.log(b)
console.log(a)
promise
1、实现promise.all
function PromiseAll(promiseArray) {
return new Promise(function (resolve, reject) {
//判断参数类型
if (!Array.isArray(promiseArray)) {
return reject(new TypeError('arguments muse be an array'))
}
let counter = 0;
let promiseNum = promiseArray.length;
let resolvedArray = [];
for (let i = 0; i < promiseNum; i++) {
Promise.resolve(promiseArray[i]).then((value) => {
counter++;
resolvedArray[i] = value;
if (counter == promiseNum) {
resolve(resolvedArray)
}
}).catch(e => reject(e));
}
})
}
// 测试
const pro1 = new Promise((res, rej) => {
setTimeout(() => {
res('1')
}, 1000)
})
const pro2 = new Promise((res, rej) => {
setTimeout(() => {
res('2')
}, 2000)
})
const pro3 = new Promise((res, rej) => {
setTimeout(() => {
res('3')
}, 3000)
})
const proAll = PromiseAll([pro1, pro2, pro3])
.then(res =>
console.log(res) // 3秒之后打印 ["1", "2", "3"]
)
.catch((e) => {
console.log(e)
})
2、实现Promise.allSeettled, 需要返回所有promise的状态和结果
function PromiseAllSettled(promiseArray) {
return new Promise(function (resolve, reject) {
//判断参数类型
if (!Array.isArray(promiseArray)) {
return reject(new TypeError('arguments muse be an array'))
}
let counter = 0;
const promiseNum = promiseArray.length;
const resolvedArray = [];
for (let i = 0; i < promiseNum; i++) {
Promise.resolve(promiseArray[i])
.then((value) => {
resolvedArray[i] = {
status: 'fulfilled',
value
};
})
.catch(reason => {
resolvedArray[i] = {
status: 'rejected',
reason
};
})
.finally(() => {
counter++;
if (counter == promiseNum) {
resolve(resolvedArray)
}
})
}
})
}
数组
1、 Array.flat
flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回
const arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]
const arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]
const arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]
//使用 Infinity,可展开任意深度的嵌套数组
const arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
如何模拟实现Array.flat?
// 使用 reduce、concat 和递归展开无限多层嵌套的数组
const arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];
function flatDeep(arr, d = 1) {
if (d > 0) {
return arr.reduce((res, val) => {
if (Array.isArray(val)) {
res = res.concat(flatDeep(val, d - 1))
} else {
res = res.concat(val);
}
return res;
}, [])
} else {
return arr.slice()
}
};
console.log(flatDeep(arr1, Infinity))
// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
如果不考虑深度, 咱们直接给他无限打平
function flatten(arr) {
let res = [];
let length = arr.length;
for (let i = 0; i < length; i++) {
if (Object.prototype.toString.call(arr[i]) === '[object Array]') {
res = res.concat(flatten(arr[i]))
} else {
res.push(arr[i])
}
}
return res
}
// 如果数组元素都是Number类型
function flatten(arr) {
return arr.toString().split(',').map(item => +item)
}
function flatten(arr){
while(arr.some(item=>Array.isArray(item))){
arr = [].concat(...arr);
}
return arr;
}
2、Array.includes
includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。
const array1 = [1, 2, 3];
console.log(array1.includes(2));
const pets = ['cat', 'dog', 'bat'];
console.log(pets.includes('cat'));
其实它有两个参数, 只不过我们平时只使用一个.
-
valueToFind 需要查找的元素值。
-
fromIndex 可选 从fromIndex 索引处开始查找 valueToFind。如果为负值,则按升序从 array.length + fromIndex 的索引开始搜 (即使从末尾开始往前跳 fromIndex 的绝对值个索引,然后往后搜寻)。默认为 0。
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
[1, 2, NaN].includes(NaN); // true
// fromIndex 大于等于数组长度
var arr = ['a', 'b', 'c'];
arr.includes('c', 3); // false
arr.includes('c', 100); // false
// 计算出的索引小于 0
var arr = ['a', 'b', 'c'];
arr.includes('a', -100); // true
arr.includes('b', -100); // true
arr.includes('c', -100); // true
3、 Array.find
find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
callback 在数组每一项上执行的函数,接收 3 个参数:
- element 当前遍历到的元素。
- index可选 当前遍历到的索引。
- array可选 数组本身。
const test = [
{name: 'lubai', age: 11 },
{name: 'xxx', age: 100 },
{name: 'nnn', age: 50}
];
function findLubai(teacher) {
return teacher.name === 'lubai';
}
console.log(test.find(findLubai));
4、Array.from
4.1 Array.from() 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
- arrayLike 想要转换成数组的伪数组对象或可迭代对象。
- mapFn 可选 如果指定了该参数,新数组中的每个元素会执行该回调函数。
4.2 Array.from() 可以通过以下方式来创建数组对象:
- 伪数组对象(拥有一个 length 属性和若干索引属性的任意对象)
- 可迭代对象(可以获取对象中的元素,如 Map和 Set 等)
console.log(Array.from('foo'));
console.log(Array.from([1, 2, 3], x => x + x));
const set = new Set(['foo', 'bar', 'baz', 'foo']);
Array.from(set);
// [ "foo", "bar", "baz" ]
const map = new Map([[1, 2], [2, 4], [4, 8]]);
Array.from(map);
// [[1, 2], [2, 4], [4, 8]]
const mapper = new Map([['1', 'a'], ['2', 'b']]);
Array.from(mapper.values());
// ['a', 'b'];
Array.from(mapper.keys());
// ['1', '2'];
所以数组去重我们可以怎么做?
function unique (arr) {
return Array.from(new Set(arr))
// return [...new Set(arr)]
}
const test = [1,1,'true','true',true,true,15,15,false,false,
undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a'];
console.log(unique(test));
function unique(arr) {
const map = new Map();
const array = []; // 数组用于返回结果
for (let i = 0; i < arr.length; i++) {
if (!map.has(arr[i])) { // 如果有该key值
array.push(arr[i]);
map.set(arr[i], true);
}
}
return array;
}
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
const array = [];
for (let i = 0; i < arr.length; i++) {
if (!array.includes(arr[i])) { //includes 检测数组是否有某个值
array.push(arr[i]);
}
}
return array
}
5、 Array.of
Array.of() 方法创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。
Array.of(7); // [7]
Array.of(1, 2, 3); // [1, 2, 3]
那怎么去模拟实现它呢?
Array.of = function() {
return Array.prototype.slice.call(arguments);
};