interview准备1- 持续更新中

605 阅读14分钟

1  事件循环

1.1为什么js有事件循环机制

js是单线程:

JavaScript的主要用途是与用户互动,以及操作DOM。如果是多线程的话会有很多复杂的问题要处理,例如两个线程同时操作DOM,一个线程删除了当前的DOM节点,另一个线程要操作当前的DOM,最后以哪个线程操作为准?为了避免这种,所以JS就设计为单线程语言。即使H5提供了web worker标准,也有很多限制,受主线程控制,是主线程的子线程。

非阻塞:

通过event loop实现

1.2 宏任务和微任务

宏任务:整体代码,setTimeout,   setInterval,   setImmediate,  I/O操作,  UI  rendering;
微任务:promise.then,  MutationObserver, process.nextTick, Object.observer;

1.3 为什么引入微任务

        宏任务按照先进先出的原则执行。 页面渲染事件,各种IO的完成事件等被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。这时突然有了高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。       

1.4 浏览器里的事件循环

关于宏任务和微任务在浏览器的执行顺序是这样的:
执行一只task(宏任务)
执行完micro-task队列(微任务)
如此循环往复执行下去

for (const macroTask of macroTaskQueue) {  
  handleMacroTask();    
  for (const microTask of microTaskQueue) {    
  	handleMicroTask(microTask);  
  }
}

1.5 node里的事件循环

大体的task(宏任务)执行顺序是这样的:

timers定时器:本阶段执行已经安排的 setTimeout() 和 setInterval() 的回调函数。
Pending callbacks待定回调:执行延迟到下一个循环迭代的 I/O 回调。
idle, prepare:仅系统内部使用。
Poll 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调
数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。
check 检测:setImmediate() 回调函数在这里执行。
close callbacks 关闭的回调函数:一些准备关闭的回调函数,如:socket.on(‘close’, …)。

宏任务和微任务在Node的执行顺序区别与node版本:

Node 10以前:
执行完一个阶段的所有任务
执行完nextTick队列里面的内容
然后执行完微任务队列的内容

Node 11以后:
和浏览器的行为一致了,都是每执行一个宏任务就执行完微任务队列。

1.6 代码输出结果

实例一:

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

async function async2() {
    console.log('async2');
}

console.log('script start');
setTimeout(function () {
    console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
    console.log('promise1');
    resolve();
}).then(function () {
    console.log('promise2');
});
console.log('script end');

输出结果:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

实例二:

console.log('start');
setTimeout(() => {
    console.log('children2');
    Promise.resolve().then(() => {
        console.log('children3');
    })
}, 0);

new Promise(function (resolve, reject) {
    console.log('children4');
    setTimeout(function () {
        console.log('children5');
        resolve('children6')
    }, 0)
}).then((res) => {
    console.log('children7');
    setTimeout(() => {
        console.log(res);
    }, 0)
})

输出结果:

start
children4
// 第一轮宏任务结束,尝试清空微任务队列,发现没有微任务
children2
// 第二轮宏任务结束,尝试清空微任务队列
children3
children5
children7
children6

实例三:

const p = function() {
    return new Promise((resolve, reject) => {
        const p1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(1);
            }, 0)
            resolve(2);
        })
        p1.then((res) => {
            console.log(res);
        })
        console.log(3);
        resolve(4);
    })
}
p().then((res) => {
    console.log(res);
})
console.log('end');

打印结果:

3
end
2
4

2   事件冒泡和捕获

2.1 基本概念

        js中事件执行的整个过程称之为事件流;分为三个阶段:事件捕获阶段,事件目标处理函数,事件冒泡;
事件捕获:自顶向下
事件冒泡:自底向上       当某个元素触发某个事件(如:click), 顶级对象document发出一个事件流,顺着dom的树节点向触发它的目标节点流去,直到达到目标元素,这个层层递进向下找目标的过程为事件捕获阶段,此过程与事件相应的函数是不会触发的。
到达目标函数,便会执行绑定在此元素上的,与事件相应的函数,即事件目标函数阶段。
最后,从目标元素起,再依次往顶层元素对象传递,途中如果有节点绑定了同名函数,这些事件所对应的函数,在此过程中便称之为事件冒泡。

2.2 window.addEventListener

        通常情况下,事件相应的函数是在冒泡阶段执行的。addEventListener的第三个参数默认为false,表示冒泡阶段执行(为true,表示捕获阶段执行)。使用e.stopPropgation()或e.cancelBubble = true(IE)可以阻断事件向当前元素的父元素冒泡。
冒泡阶段:

window.addEventListener('click', () => {
});

捕获阶段:

window.addEventListener('click', () => {
}, true);

        现在有这么一个场景, 一个历史⻚面, 上面有若干按钮等点击逻辑, 每个按钮都有自己的click事件。现在新需求来了, 突然给每一个访问用户添加了banned这个属性, 如果为true, 则代表此用户被封禁了。被封禁用户不可操作⻚面上的任何内容, 点击⻚面内的任何一处, 都弹窗提示您已被封禁;

window.addEventListener('click', () => {
    if (banned === true) {
        e.stopPropgation();
    }
}, true);

3  说一下防抖和截流

3.1  防抖

定义

防抖:指触发事件后在规定时间内回调函数只能执行一次,如果在规定时间内又触发了该事件,会重新开始计算规定时间。总结就是延时执行。

应用场景:

两个条件:

1,如果用户的连续操作会导致频繁的事件回调(可能引起页面卡顿)
2,用户只关心最后一次操作所返回的结果
主要用在以下场景:
1,input输入搜索
2,按钮点击:收藏,点赞等

实现:

function debounce(fn, wait, immediate) {
    let timer = null;
    return function(...args) {
        // 立即执行的功能(timer为空表示首次触发)
        if (immediate && !timer) {
            fn.apply(this, args);
        }
        // 有新的触发,则把定时器清空
        timer && clearTimeout(timer);
        // 重新计时
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, wait)
    }
}

3.2  节流

定义
当持续触发事件时,在规定时间内只能调用一次回调函数。如果在规定的时间内又触发了该事件,则什么也不做,也不会重置定时器。
与防抖比较:
防抖是将多次执行变为最后一次执行,节流是将多次执行变为在规定事件内只执行一次。
应用场景:
resize,scroll.

实现:

// 时间戳写法,第一次立即执行
function throttle(fun, delay = 500) {
    let previous = 0;  //记录上一次触发的时间戳.这里初始设为0,是为了确保第一次触发产生回调
    return function(args) {
        let now = Date.now(); //记录此刻触发时的时间戳
        let that = this;
        let _args = args;
        if (now - previous > delay) {  //如果时间差大于规定时间,则触发
            fun.apply(that, _args);
            previous = now;
        }
    }
}

// 定时器写法:
function throttle(fun, delay = 500) {
    let timer;
    return function(args) {
        let that = this;
        let _args = args;
        if (!timer) {  
            //如果定时器不存在,则设置新的定时器,到时后,才执行回调,并将定时器设为null
            timer = setTimeout(function(){
                timer = null;
                fun.apply(that, _args)
            }, delay)
        }

    }
}

//时间戳+定时器版: 实现第一次触发可以立即响应,结束触发后也能有响应 (该版才是最符合实际工作需求)
//该版主体思路还是时间戳版,定时器的作用仅仅是执行最后一次回调
function throttle(fun, delay = 500) {
     let timer = null;
     let previous = 0;
     return function(args) {
             let now = Date.now();
             let remaining = delay - (now - previous); //距离规定时间,还剩多少时间
             let that = this;
             let _args = args;
             clearTimeout(timer);  //清除之前设置的定时器
              if (remaining <= 0) {
                    fun.apply(that, _args);
                    previous = Date.now();
              } else {
                    timer = setTimeout(function(){
                        fun.apply(that, _args)
                    }, remaining); 
                //因为上面添加的clearTimeout.实际这个定时器只有最后一次才会执行
              }
      }
 }

4  Promise

4.1  实现一个Promise.all

function PromiseAll(promiseArr) {
    return new Promise((resolve, reject) => {
        if(!Array.isArray(promiseArr)) {
            return reject(new Error('传入的参数必须是数组!'));
        }
        const res = [];
        const promiseNums = promiseArr.length;
        let counter = 0;
        for (let i = 0; i < promiseNums; i++) {
            Promise.resolve(promiseArr[i]).then(function (value) {
                counter++;
                res[i] = value;
                if (counter === promiseNums) {
                    return resolve(res);
                }
            }, function (reason) {    
                return reject(reason);
            });
        }
    })
}

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('1');
    }, 1000)
});

const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('2');
    }, 2000)
})

const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('3');
    }, 3000)
});

const arr = [p1, p2, p3];

const proAll = PromiseAll(arr)
    .then(res => {
        console.log(res);
    })
    .catch((e) => {
        console.log(e);
    });

// 3s后打印 ["1", "2", "3"]

4.2  手写Promise.race

Promise.race与Promise.all不同。它是数组中只有一个对象(最早改变状态) resolve或reject时,就改变自身的状态,并执行响应的回调。

function promiseRace(promises) {
	if (!Array.isArray(promises)) {
		throw new Error("promises must be an array")
	}
	return new Promise(function (resolve, reject) {
		promises.forEach(p =>
			Promise.resolve(p).then(data => {
				resolve(data)
			}, err => {
				reject(err)
			})
		)
	})
}

4.3 缓存promise,使用装饰器写法

const cacheMap = new Map();
function enableCache(target, name, descriptor) {
    const val = descriptor.value;
    descriptor.value = async function (...args) {
        const cacheKey = name + JSON.stringify(args);
        if(!cacheMap.get(cacheKey)) {
            const cacheValue = Promise.resolve(val.apply(this, args)).cache(_ => {
                cacheMap.set(cacheKey, null);
            })
            cacheMap.set(cacheKey, cacheValue);
        }
        return cacheMap.get(cacheKey);
    }
    return descriptor;
}
class PromiseClass {
    @enableCache
    static async getInfo() {}
}

PromiseClass.getInfo();
PromiseClass.getInfo();

4.4 实现promise的并发控制头条前端笔试题-实现一个带并发限制的promise异步调度器

实现如下:

function Schedule() {
    this.list = [];
    this.add = function(promiseCreator) {
        this.list.push(promiseCreator)
    }
    this.maxCount = 2;
    let tmpRunIndex = 0;
    this.taskStart = function() {
        for (let i = 0; i < this.maxCount; i++) {
            request.bind(this)();
        }
    }
    function request() {
        if(!this.list || !this.list.length || tmpRunIndex>=this.maxCount) {
            return;
        }
        tmpRunIndex++;
        this.list.shift()().then(() => {
            tmpRunIndex--;
            request.bind(this)();
        })
    }
}

function timeout(time) {
    return new Promise(resolve => {
        setTimeout(resolve, time)
    })
}

var schedule = new Schedule();
function addTask(time, order) {
    schedule.add(() => timeout(time).then(() => console.log(order)));
}

addTask(1000, 1);
addTask(500, 2);
addTask(300, 3);
addTask(400, 4);

schedule.taskStart();

运行结果:

2
3
1
4

4.5 如何把大象放进冰箱

开门/放大象/关门都是异步操作。 实现把大象放进冰箱

console.time();

const openDoor = (cb) => {
  setTimeout(cb, 1000)
}

const putIn = (cb) => {
  setTimeout(cb, 3000)
}

const closeDoor = (cb) => {
  setTimeout(cb, 1000);
}

const done = () => {
  console.timeEnd();
  console.log('done');
}

回调函数实现

openDoor(() => putIn(() => closeDoor(() => done())));

promise实现

console.time();

const openDoor = () => new Promise(res => {
  setTimeout(res, 1000);
})

const putIn = () => new Promise(res => {
  setTimeout(res, 3000);
})

const closeDoor = () => new Promise(res => {
  setTimeout(res, 1000)
})

const done = () => new Promise(res => {
  console.timeEnd();
  console.log('done');
  res();
})

openDoor().then(putIn).then(closeDoor).then(done);

4.6  如何实现promise的链式调用

要实现链式调用,可以有两种方式,返回this,或者返回一个和自己类似的结构。

返回this

class Test1 {
  then() {
    console.log(666);
    return this;
  }
}

var a = new Test1();
a.then().then().then();

返回类似的结构

class Test2 {
  then() {
    console.log(777);
    return new Test2();
  }
}

var b = new Test2();
b.then().then().then();

promise采用方式二;

4.7 理解promise中then和catch;

画出以下异步函数执行的可能性;

asyncThing1()
  .then(function() {
    return asyncThing2();
  })
  .then(function() {
    return asyncThing3();
  })
  .catch(function(err) {
    return asyncRecovery1();
  })
  .then(
    function() {
      return asyncThing4();
    },
    function(err) {
      return asyncRecovery2();
    }
  ).catch(function(err) {
    console.log('Don't worry about it");
  })
  .then(function() {
    console.log('All done");
  });

绿色代表执行的promise路径,红色代表拒绝的promise路径;

4  深浅拷贝

4.1  定义

浅拷贝:
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝:
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

4.2  实现

简单版:日常项目中可以这样实现

JSON.parse(JSON.stringify(source));

面试版:

function deepClone(obj, map = new WeakMap()) {
	if(typeof obj !== 'object') return obj;
	// 过滤特殊情况
	if(obj === null) return null;
	if(obj instanceof RegExp) return new RegExp(obj);
	if(obj instanceof Date) return new Date(obj);
	if(obj instanceof Function) return new Function(obj);

	// 不直接创建空对象目的,克隆的结果和之前保持相同的所属类
	let newObj = new obj.constructor;
	// 考虑循环引用
        if(map.get(obj)) {
  	    return map.get(obj);
        }
        map.set(obj, newObj);
	for (let key in obj) {
		if(obj.hasOwnProperty(key)) {
			newObj[key] = deepClone(obj[key], map);
		}
	}
	return newObj;
}
const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};
target.target = target;
const obj2 = deepClone(target);

5  代码运行结果

5.1 变量相关

1,写出以下代码运行结果

let a = {}, b = '0', c = 0;
a[b] = '苹果';
a[c] = '栗子';
console.log(a[b]); //栗子
let a = {}, b = Symbol('1'), c = Symbol('1');
a[b] = '香蕉';
a[c] = '草莓';
console.log(a[b]); //香蕉
let a = {}, b = {n: '1'}, c = {m: '2'};
a[b] = '西瓜';
a[c] = '芒果';
console.log(a[b]); //芒果

2,请写出a, b, c, d, e, f, h所对应的值

var a = 1 || {a: 1};
var b = 1 && {a: 1};
var c = {} || {a: 1};
var d = {} && {a: 1};
var e = {} || g;
var f = {} && g;
var h = !a || {a: 1};

打印结果:

1
{a: 1}
{}
{a: 1}
{}
undefined
{a: 1}

5.2  类型转换相关

var a = ?;
if(a == 1 && a == 2 && a == 3) {
    console.log(1);
}

js里面==比较遵循以下规则,==数据类型不一致会进行类型转换后比较;
1,对象==字符串,对象.toString()变为字符串;
2,null==undefined相等,但是和其他值比较就不相等了;
3,NaN==NaN 不相等
4,剩下的都转换为数字, 例如[10] == 10 返回true; [10].toString = '10', Number("10") = 10;

实现方式

// 重写toString方法
var a = {
    i: 0,
    toString() {
        return ++this.i;
    }
};
if(a == 1 && a == 2 && a == 3) {
    console.log(1);
}

//重写valueOf
var a = {
    i: 0,
    valueOf() {
        return ++this.i;
    }
};
if(a == 1 && a == 2 && a == 3) {
    console.log(1);
}

// 数据劫持
var i = 0;
Object.defineProperty(window, 'a', {
    get() {
        return ++i;
    }
});
if(a == 1 && a == 2 && a == 3) {
    console.log(1);
}

// 注意下面的写法是错误的
var a = 0;
Object.defineProperty(window, 'a', {
    get() {
        // Uncaught TypeError: Cannot redefine property: a
        //at Function.defineProperty拦截器中不能再次获取当前属性
        return ++a;
    }
});
if(a == 1 && a == 2 && a == 3) {
    console.log(1);
}


//数组实现
var a = [1, 2, 3];
a.toString = a.shift;
if(a == 1 && a == 2 && a == 3) {
    console.log(1);
}

5.3  面向对象相关

function Foo() {
	getName = function() {
		console.log(1);
	};
	return this;
}
Foo.getName = function() {
	console.log(2);
}
Foo.prototype.getName = function() {
	console.log(3)
}
var getName = function() {
	console.log(4);
}
function getName() {
	console.log(5)
}
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

执行过程如下图所示

代码输出结果:

2
4
1
1
2
3
3

6  实现new

new运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

实现如下:

步骤:
创建一个新对象;
将构造函数的作用域赋给新对象(因此 this就指向了这个新对象);
执行构造函数中的代码(为这个新对象添加属性);
返回新对象

function myNew() {
    var obj = new Object();
    var constructor = Array.prototype.shift.call(arguments);
    obj.__proto__ = constructor.prototype;
    var ret = constructor.apply(obj, arguments);
    return typeof ret === 'object' && ret !== null ? ret : obj;
}

function Otaku (name, age) {
    this.name = name;
    this.age = age;

    this.habit = 'Games';
}

Otaku.prototype.strength = 60;

Otaku.prototype.sayYourName = function () {
    console.log('I am ' + this.name);
}

var person = myNew(Otaku, 'Kevin', '18');

console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60

person.sayYourName();

//Kevin
//Games
//60
//I am Kevin

7  实现call,apply

7.1 call 和 apply区别

传给fun的参数写法不同:

  • apply是第2个参数,这个参数是一个数组:传给fun参数都写在数组中。

  • call从第2~n的参数都是传给fun的。

7.2  call

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

Function.prototype.call2 = function(context) {
    var context = context || window;
    context.fn = this;
    var args = [];
    for (var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    var result = eval('context.fn(' + args + ')');
    delete context.fn;
    return result;
}

var value = 2;

var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call(null);
console.log(bar.call2(obj, 'kevin', 18));

7.3   apply

Function.prototype.apply2 = function(context, arr) {
    var context = context || window;
    context.fn = this;
    var result;
    if(!arr) {
        result = context.fn();
    } else {
        var args = [];
        for (var i = 0; i < arr.length; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')');
    }
    delete context.fn;
    return result;
}
function add(c,d){
    return this.a + this.b + c + d;
}

var s = {a:1, b:2};
console.log(add.call(s,3,4)); // 1+2+3+4 = 10
console.log(add.apply(s,[5,6]));
console.log(add.apply2(s,[5,7]));

8  bind

8.1  定义

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

8.2 与call/apply的区别

执行时

  • call/apply改变了函数的this上下文后立即执行该函数
  • bind则是返回改变了上下文后的函数,不立即执行该函数

返回值:

  • call/apply 返回fun的执行结果
  • bind返回fun的拷贝,并指定了fun的this指向,保存了fun的参数。

8.3  实现

Function.prototype.bind2 = function (context) {
    if(typeof this !== "function") {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var fNOP = function () {};
    var fbound = function () {
        self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments)));
    }
    fNOP.prototype = this.prototype;
    fbound.prototype = new fNOP();
    return fbound;
}

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);

}

var bindFoo = bar.bind2(foo, 'daisy');
bindFoo('18');
//1
//daisy
//18

9  实现一个数据绑定

9.1  Object.defineProperty方式

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  姓名:<span id='spanName'></span>
  <br />
  <input type="text" id="inputname" />
  <script type="text/javascript">
    let obj = {
      name: ''
    }
    let newObj = JSON.parse(JSON.stringify(obj));
    Object.defineProperty(obj, 'name', {
      get() {
        return newObj.name;
      },
      set(val) {
        if(val === newObj.name) return;
        newObj.name = val;
        observer();
      }
    });
    function observer() {
      spanName.innerHTML = obj.name;
      inputname.value = obj.name;
    }
    inputname.oninput = function() {
      obj.name = this.value;
    }
  </script>
</body>
</html>

9.2  Proxy实现

let obj = {
      name: ''
    }
    obj = new Proxy(obj, {
      get(target, prop) {
        return target[prop];
      },
      set(target, prop, value) {
        target[prop] = value;
        observer();
      }
    });
    function observer() {
      spanName.innerHTML = obj.name;
      inputname.value = obj.name;
    }
    inputname.oninput = function() {
      obj.name = this.value;
    }

10  数组去重

const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];
// => [1, '1', 17, true, false, 'true', 'a', {}, {}]

10.1  Set实现

const res1 = Array.from(new Set(arr));

10.2  indexOf实现

方式一:

// 拿出当前项和后面的内容进行比较
const unique1 = arr => {
    for (let i = 0; i < arr.length - 1; i++) {
        let item = arr[i],
            args = arr.slice(i + 1);
        if(args.indexOf(item) > -1) {
            //包含:删除当前项
            //splice删除:原来数组改变,这样如果i继续++,会产生数组塌陷
            //性能不好,当前项一旦删除,后面索引都要变
            arr.splice(i, 1);
            i--;
        }
    }
    return arr;
}

方式二:

const unique2 = arr => {
    let arr1 = [...arr];
    for (let i = 0; i < arr.length - 1; i++) {
        let item = arr[i],
            args = arr.slice(i + 1);
        if(args.indexOf(item) > -1) {
            arr1.splice(i, 1);
        }
    }
    return arr1;
}

方式三:

const unique3 = arr => {
    let arrnew = [];
    for (let i = 0; i < arr.length; i++) {
        let item = arr[i],
            args = arr.slice(i + 1);
        if(args.indexOf(item) > -1) {
            continue;
        } else {
            arrnew.push(item);
        }
    }
    return arrnew;
}

方式四:

const unique4 = arr => {
    for (let i = 0; i < arr.length; i++) {
        let item = arr[i],
            args = arr.slice(i + 1);
        if(args.indexOf(item) > -1) {
            arr[i] = null;
        } 
    }
    arr = arr.filter(item => item !== null);
    return arr;
}

方式五:

const unique5 = arr => {
    for (let i = 0; i < arr.length; i++) {
        let item = arr[i],
            args = arr.slice(i + 1);
        if(args.indexOf(item) > -1) {
            //用最后一项替换
            arr[i] = arr[arr.length - 1];
            arr.length--;
            i--;
        } 
    }
    return arr;
}

10.3  include实现

const unique6 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!res.includes(arr[i])) res.push(arr[i]);
  }
  return res;
}

10.4  filter实现

const unique7 = arr => {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index;
  });
}

10.5 Map实现

const unique8 = arr => {
  const map = new Map();
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!map.has(arr[i])) {
      map.set(arr[i], true)
      res.push(arr[i]);
    }
  }
  return res;
}

11  数组扁平化

let arr = [1, 2, [3, 4], [5, [6, 7], 8], 9];

11.1  flat实现

const res1 = arr.flat(Infinity);

11.2  reduce实现

const flatten = arr => {
    return arr.reduce((prev, cur) => {
        return prev.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, [])
}
const res2 = flatten(arr);

11.3  递归实现

const flatten2 = (arr) => {
    var result = [];
    for (let i = 0, len = arr.length; i < len; i++) {
        if(Array.isArray(arr[i])) {
            result = result.concat(flatten2(arr[i]));
        } else {
            result.push(arr[i]);
        }
    }
    return result;
}
const res3 = flatten2(arr);

11.4  转换为字符串

arr = arr.toString().split(',').map(item => parseFloat(item));

11.5  some实现

let flatten = arr => {
    while(arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}
let res1 = flatten(arr);

12 web解决数组塌陷的办法

在处理数组的时候,如果想要删除干净一个数组按照正常的方式肯定是循环遍历,遍历过程中一个一个删除,实际操作中会出现删除不干净的情况,这就是数组塌陷;

方案一:设置删除起始位置为0

let arr = [1, 4, 6, 44, 55, 43];
let length=arr.length
for(var i=0;i<length;i++){
   arr.splice(0,1)
}
console.log(arr);
// [];

要想删除干净,需要将数组的长度先单独保存(let length=arr.length),不然数组的长度会随着数组的变化而变化,从而4>3,后面三个删除不了,如下所示:

let arr = [1, 2, 3, 4, 5, 6, 7, 8];
for(var i=0;i<arr.length;i++){
    arr.splice(0,1)
}
console.log(arr);
//[5, 6, 7, 8]

方案二:从后面开始删除,倒着删除

let arr = [1, 2, 3, 4, 5, 6, 7, 8];
for(var i=arr.length-1;i>=0;i--){
   arr.splice(i,1)
}
console.log(arr);
// []

方案三:让i永远成为0, 永远删除的是第一个元素

let arr = [1, 2, 3, 4, 5, 6, 7, 8];
for(var i=0;i<arr.length;i++){
    arr.splice(i,1);
    i--;
}
console.log(arr);
// []

13  实现下面需求

13.1   实现一个选择器

function $attr(property, value) {
    // 获取当前内容的所有标签
    let elements = document.getElementsByTagName('*'),
        arr = [];
    elements = Array.from(elements);
    elements.forEach(item => {
        let itemValue = item.getAttribute(property);
        // 样式类属性名要特殊处理
        if(property === 'class') {
            new RegExp("\\b" + value + "\\b").test(itemValue) ? arr.push(item) : null;
            return;
        }
        if(itemValue === value) {
            arr.push(item);
        }
    });
    return arr;
}
console.log($attr('class', 'box'));

13.2  实现(5).add(3).minus(2)=6

(function () {
    //每一个方法执行完,都要返回Number这个类的实例,才能继续调用实例上的方法
    // 链式写法
    function check(n) {
        n = Number(n);
        return isNaN(n) ? 0 : n;
    }
    function add(n) {
        n = check(n);
        return this + n;
    }
    function minus(n) {
        n = check(n);
        return this - n;
    }
    Number.prototype.add = add;
    Number.prototype.minus = minus;
})();
console.log((5).add(3).minus(2));