🔥 没错我在试图教会你——JavaScript手写题

310 阅读18分钟

不知道有没有跟我一样每次面试前都要去现找一些题去看的小伙伴,基于这一痛点呢我想还是总结一下,这样显得更有效率也更加系统!写这篇文章也是为了准备面试所以也会尽可能写的详细一点。

遍历对象的方法有哪些

  • for in
  • Object.keys()
  • Object.values()
  • Object.entries()
  • Reflect.ownKeys()

for in

该方法会遍历对象中的所有可枚举属性,也就是说即使是在原型中的可枚举属性也会被遍历到。

const obj={
    a:1,
    b:2,
    c:3,
    [Symbol('s')]:6
}
obj.__proto__.d=4;
obj.__proto__.f=5;
for(const key in obj){
    console.log(key) // a,b,c,d,f
}

Object.keys()

该方法会获取对象自身可枚举属性的键名(属性名),并以数组的形式返回这些键名。

// ...重复部分省略
console.log(Object.keys(obj)); // ['a','b','c']

Object.values()

该方法会获取对象自身可枚举属性的属性值,并以数组的形式返回这些值。

// ...重复部分省略
console.log(Object.values(obj)); // [1,2,3]

Object.entries()

其实 keys、values、entries 这三个方法都是相对的,keys 用于获取键,values 用于获取值,那entries 当然就是用来获取对象的键值对形式的了。

// ...重复部分省略
console.log(Object.entries(obj)); // [['a',1],['b',2],['c',3]]

Reflect.ownKeys()

该方法会以数组的方式返回对象自身的所有属性包括不可枚举属性。

// ... 重复部分省略
console.log(Reflect.ownKeys(obj)) // ['a','b','c','Symbol(s)']

一、手写new方法

使用new生成对象实例总共经历了以下四个步骤:

  1. 创建一个新的对象;
  2. 将新对象的__proto__属性指向构造函数的prototype;
  3. 执行构造函数,并把this指向新创建的对象;
  4. 若有返回值则判断是否为引用类型,如果是则返回引用类型的值,否则返回新创建的对象(null虽然也是对象,但仍然会返回实例对象);
    function mynew(){
          // 创建一个新的对象
          const obj = {};
          // 从参数中获取构造函数
          const func = [].shift.call(arguments);
          obj.__proto__ = func.prototype;
          // 执行构造函数,并把this指向新创建的对象
          const res = func.apply(obj,[...arguments])
          // 判断返回值类型如果为引用类型则返回对应的引用类型的值,否则返回新创建的对象
          return res instanceof Object ? res : obj
    }
    
    // demo
    function Person(name,age){
        this.name = name;
        this.age = age;
        // return {name:'邓紫棋',age:18}
    }
    Person.prototype.fn = function(){
        console.log("我可以陪你去流浪~~~")
    }
    const p = mynew(Person,'薛之谦',20) // {name:'薛之谦',age:20}
    p.fn(); // 我可以陪你去流浪~~~   如果Person中return的是引用类型则没有fn方法

二、手写instanceof方法

instanceof是用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,也可以用来检测一个实例对象是由哪个构造函数构造出来的。

function myInstanceof(obj,constructor){
    // 如果是传入的是基本数据类型直接返回false
    if(typeof obj != 'object' || obj == null)return false;
    let proto = obj.__proto__; // 也可以使用Object.getPrototypeOf()方法获取对象原型
    // 
    while(true){
        // 已经查到了最顶层
        if(proto == null)return false;
        // 说明在原型链上
        if(proto == constructor.prototype)return true;
        // 再查找到顶层对象之前依然没有找到则继续向上查找
        proto = proto.__proto__;
    }
}

console.log(myInstanceof({},Object)); // true
console.log(myInstanceof({},Array)); // false

三、手写call、apply、bind方法

三个方法的作用相同:改变函数运行时的this值。

三个方法的对比:

  1. callbind方法的参数是一个参数列表,apply方法的参数是一个数组
  2. callapply方法会立即执行函数,bind会返回一个新的函数,需要手动调用此函数才会获得执行结果

call

// 为了让所有函数都能调用到我们自定义的方法,需要把方法定义到Function的原型上
// myCall方法中的this指向fn函数,thisArg指向的是目标对象
Function.prototype.myCall = function(thisArg,...args){
        // 如果传入的目标对象是undefined或null,就将this指向全局对象
        thisArg = thisArg || window
        // 目的是让fn成为目标对象的方法来运行,这样this便指向了目标对象(核心思路:根据谁调用函数this就指向谁的原则)
        thisArg.f = this;
        // 运行这个方法并传入剩余参数
        let result = thisArg.f(...args)
        // 返回值同fn原函数一样
        return result;
        
        
        /**
        * 到这里call的基本功能就完成了,但还存在一些问题:
        * 目标对象上会永远存在我们自定义的f属性,并且如果多个函数调用这个方法,而目标对象也相同,
          则存在目标对象的f属性被覆盖的可能
        * 我们可以通过以下两种方式解决:
        * 1、使用Symbol数据类型来定义对象的唯一属性名
        * 2、使用delete操作符删除对象中的某个属性
        * (一下代码与上面没有关联)
        */
        // 如果传入的目标对象是undefined或null,就将this指向全局对象
        thisArg = thisArg || window
        // 生成唯一属性名,解决覆盖的问题
        const prop = Symbol();
        // 注意这里不能使用.
        thisArg[prop] = this;
        // 运行这个方法,传入剩余参数,同样不能用.
        let result = thisArg[prop](...args);
        // 运行完删除属性
        delete thisArg[prop]
        return result;
}
// demo
function fn(name,age){
    console.log(name,age); // '薛之谦',20
    return '我的心愿是世界和平!'
}
let p = fn.myCall(obj,'薛之谦',20)
console.log(p); // 我的心愿是世界和平!

apply

apply和call的实现思路一样,只是传参方式不同

Function.prototype.myApply = function (thisArg, args) {
    thisArg = thisArg || window;
    // 判断是否接收参数,若未接收参数,替换为[]
    args = args || []
    const prop = Symbol();
    thisArg[prop] = this;
    let result = thisArg[prop](...args);
    delete thisArg[prop];
    return result;
}

bind

bind相对来说就比较复杂一点了,我们一点点来。

1、bind方法会返回一个改变this指向后的新方法。

// 原bind方法改变this指向demo
const name = "柚子";
const obj = {
    name:"刑天铠甲"
}
function person(){
    console.log(this.name)
}

person();   //柚子
const p = person.bind(obj)
p()  //刑天铠甲



//v1 首先根据这一特点来实现第一版的bind方法
Function.prototype.myBind = function(thisArg){
    // 获取调用bind的函数,也就是绑定函数
    let self = this;
    // 返回一个用来改变this指向的函数
    return function(){
        self.apply(thisArg);
    }
}

注意点(当然也可以用箭头函数)为什么要在return前定义self来保存this?因为我们需要利用闭包将this(即person)保存起来,使得myBind方法返回的函数在运行时的this值能够正确地指向person

const p = function(){
    return this.apply(thisArg) // 若不用self保存this此时this指向window
}
p();

2、bind方法函数的参数可以分多次传入,即可以在bind中传入一部分参数,在执行返回的函数的时候,再传入另一部分参数。

// 原bind方法分段传参demo
const name = "柚子";
const obj = {
    name: "刑天铠甲"
}
function person(age, sex, job) {
    console.log(this.name, age, sex, job)
}
person(20, 'female', '藏修者') //柚子 20 female 藏修者

const p = person.bind(obj, 20, 'male')
p('消灭幽冥魔!'); //刑天铠甲 20 male 消灭幽冥魔!



// v2 分段接受参数
Function.prototype.myBind = function(thisArg){
    // 获取调用bind的函数,也就是绑定函数
    let self = this;
    //用slice方法取第二个到最后一个参数(获取除this对象以外的所有参数)
    let args = [...arguments].slice(1);
    // 返回一个用来改变this指向的函数
    return function(){
        // 这里的arguments获取的是指向bind方法返回的函数中传入的参数
        let innerArgs = [...arguments];
        self.apply(thisArg,args.concat(innerArgs));
    }
}

3、bind方法返回函数中的返回值。

// 原bind方法返回值demo
const name = "欢迎";
const obj = {
    name: "飞影铠甲"
}

function person(age, sex, job) {
    return {
        name: this.name,
        age,
        sex,
        job
    }
}
const huan = person(22, 'female', 'leader')
console.log(huan)    //{name: '欢迎', age: 22, sex: 'male', job: '打败幽冥魔'}

const fei = person.bind(obj, 22, 'male');
const res = fei('打败幽冥魔');
console.log(res)    //{name: '飞影铠甲', age: 22, sex: 'male', job: '打败幽冥魔'}

// v3 获取函数的返回值
Function.prototype.myBind = function(thisArg){
    // 获取调用bind的函数,也就是绑定函数
    let self = this;
    //用slice方法取第二个到最后一个参数(获取除this对象以外的所有参数)
    let args = [...arguments].slice(1);
    // 返回一个用来改变this指向的函数
    return function(){
        // 这里的arguments获取的是指向bind方法返回的函数中传入的参数
        let innerArgs = [...arguments];
        // 拼接bind方法传入的参数和bind方法返回的函数中传入的参数,统一在最后通过apply执行
        // 返回一下执行结果即可
        return self.apply(thisArg,args.concat(innerArgs));
    }
}

四、手写Promise对象

Promise 是异步编程的一种解决方案,首先我们要想知道promise都有哪些特点

  • promise对象的状态不受外界影响,promise对象代表一个异步操作并且有三种状态:pendingfulfilledrejected,只有异步操作的结果可以决定当前是哪一种状态并且状态不可逆;
  • promise执行的过程中如果出现报错,那么最后的状态也将会是rejected
  • then方法的第一个参数是fulfilled状态的回调,第二个参数是rejected状态的回调函数,它们都是可选的,并且会返回一个新的Promise实例
const p = new Prmoise((resolve,reject)=>{
    resolve(123);
})

以上是promise的基本用法,下面我们用class来实现。

// 首先我们可以看到Promise接受一个函数作为参数并且函数的参数也是一个函数,于是
// 我们可以这样写
class myPromise{
    constructor(executor){
    // 同时这里我们还可以获取到resolve和reject方法调用后传递的参数
        const resolve=(data)=>{};
        const reject=(err)=>{};
        executor(resolve,reject)
    }
}

现在我们已经把promise的基本形式写出来了,下面就开始实现它的一些语法特点。

1、改变promise的状态并且状态改变后不可逆

// promise状态的改变是在resolve或reject方法调用后才会改变的,所以
// 在实现这一操作需要在定义好的resolve和reject方法中来实现
// 首先我们需要声明一个存储状态的变量,由于这个变量实例对象并不需要用到
// 所以我们可以为当前的类设置一个私有属性
// 下面不再一一说明 #+属性名是ES6提供的类的私有属性
// 详见:https://es6.ruanyifeng.com/#docs/class#%E7%A7%81%E6%9C%89%E6%96%B9%E6%B3%95%E5%92%8C%E7%A7%81%E6%9C%89%E5%B1%9E%E6%80%A7
class myPromise{
    #state = 'pending'
    constructor(executor){
        const resolve = (data) => {
            this.#state = 'fulfilled'
        };
        const reject = (err) => {
            this.#state = 'rejected'
        };
        executor(resolve,reject)
    }
}


// 以上直接将状态赋值state变量的方式成为硬编码,后面我们会多次用到这些状态,
// 所以我们可以将他们定义成常量
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class myPromise {
    #state = PENDING;
    constructor(executor) {
        const resolve = (data) => {
            this.#state = FULFILLED;
        };
        const reject = (err) => {
            this.#state = REJECTED;
         };
        executor(resolve, reject)
    }
}


// 改变了promise的状态后下一步就需要固定promise的状态
// 其实很简单,只需要判断当前状态是不是pending即可,若不是直接返回
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class myPromise {
    #state = PENDING;
    constructor(executor) {
        const resolve = (data) => {
            if (this.#state != PENDING) return;
            this.#state = FULFILLED;
        };
        const reject = (err) => {
            if (this.#state != PENDING) return;
            this.#state = REJECTED;
        };
        executor(resolve, reject)
    }
}


// 仔细看这里的代码是不是进行一个简单的封装操作,我们可以单独写一个改变状态的方法
// 然后分别在里面进行调用
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class myPromise {
    #state = PENDING;
    constructor(executor) {
        const resolve = (data) => {
            this.#changeState(FULFILLED, data)
        };
        const reject = (err) => {
            this.#changeState(REJECTED, err)
        };
        executor(resolve, reject)
    }
    // 这里同样是只能在类里面使用的方法,因为外部并不需要获取到此方法
    #changeState(state, result) {
        if (this.#state != PENDING) return;
        this.#state = state;
    }
}

// 状态改变后还可以获取resolve或reject方法中传递的参数 例如resolve(123)
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class myPromise {
    #state = PENDING;
    #result = undefined; // 初始值可以设为undefined
    constructor(executor) {
        const resolve = (data) => {
            this.#changeState(FULFILLED, data)
        };
        const reject = (err) => {
            this.#changeState(REJECTED, err)
        };
        executor(resolve, reject)
    }
    #changeState(state, result) {
        if (this.#state != PENDING) return;
        this.#state = state;
        this.#result = result; //在这里先保存一下传递过来的结果
        console.log(this.#state,this.#result); // 分别进行测试打印
    }
}

// demo1
 const p = new myPromise((resolve,reject)=>{
    resolve(123); // fulfilled 123
})

//demo2
 const p = new myPromise((resolve,reject)=>{
    reject('失败了');  // rejected 失败了
    resolve('成功了'); // 不再执行
})

2、还有一种情况就是在原生promise执行的过程中如果出现报错,那么最后的状态也将会是rejected

// 先看一下原生promise
const p = new Promise((resolve,reject)=>{
    throw '报错了'
})
console.log(p);
//[[Prototype]]: Promise
//[[PromiseState]]: "rejected"
//[[PromiseResult]]: "报错了"

// 思路就是我们只需要对这个错误进行捕获,如果捕获到了就直接将状态变为rejected即可
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class myPromise {
    #state = PENDING;
    #result = undefined;
    constructor(executor) {
        const resolve = (data) => {
            this.#changeState(FULFILLED, data)
        };
        const reject = (err) => {
            this.#changeState(REJECTED, err)
        };
        // 对这个方法的调用使用try catch捕获
        try {
            executor(resolve, reject)
        } catch (err) {
            this.#changeState(REJECTED,err)
        }
    }
    #changeState(state, result) {
        if (this.#state != PENDING) return;
        this.#state = state;
        this.#result = result
        console.log(this.#state, this.#result) // rejected 我报错了
    }
}

const p = new myPromise((resolve,reject)=>{
       throw '我报错了'
}) 

但是这个错误如果是一个异步中的错误将无法对其进行捕获,在原生promise中也是无法进行捕获,这里也是一个小的考点哦!例如:

const p = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        throw 123
    },1000)
})
console.log(p);
//[[Prototype]]: Promise
//[[PromiseState]]: "pending"
//[[PromiseResult]]: undefined

3、then方法

首先then方法接受两个回调函数分别是成功和失败后的回调,并返回一个promise对象 ,于是可以写出下面的代码:

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class myPromise {
    #state = PENDING;
    #result = undefined;
    constructor(executor) {
        const resolve = (data) => {
            this.#changeState(FULFILLED, data)
        };
        const reject = (err) => {
            this.#changeState(REJECTED, err)
        };
        try {
            executor(resolve, reject)
        } catch (err) {
            this.#changeState(REJECTED, err)
        }
    }
    #changeState(state, result) {
        if (this.#state != PENDING) return;
        this.#state = state;
        this.#result = result
        console.log(this.#state, this.#result)
    }
    then(onFulFilled,onRejected){
        return new myPromise((resolve,reject)=>{
            
        })
    }
}

那么现在就面临了两个比较大的问题:

1、onFulfilled和onRejected两个函数什么时候调用

2、如何获取返回promise的状态(成功还是失败)

第一个问题稍微简单一点,只需要根据当前promise的状态来调用即可

then(onFulfilled,onRejected){
    return new myPromise((resolve,reject) => {
        // 如果成功状态就调用成功的回调函数并把成功后的信息传入,失败亦是
        if(this.#state == FULFILLED){
            onFulfilled(this.#result)
        }else if(this.#state == REJECTED){
            onRejected(this.#result)
        } 
    })
}

// demo
const p = new myPromise((resolve,reject)=>{
    resolve(123)
})
p.then((res)=>{
    console.log(res) // 123
},(err)=>{
    console.log(err)
})

那如果resolve(123)没有很快完成呢

const p = new myPromise((resove,reject)=>{
    setTimeout(()=>{
        resolve(123)
    },1000)
})

这个时候then方法其实是不会获取到成功后传递的数据的,因为在resolve执行之前then方法已经被执行过了,那这个问题应该如何解决呢?

我们的目的其实就是在resolve方法执行后立即执行then方法中的回调函数,现在函数并没有立即执行是因为在then方法中并不知道当前的promise处于什么状态(因为resolve方法被延迟执行了,所以then方法会在resolve方法之前被执行,resolve方法执行完之后then方法已经不会被执行了)

找到问题所在后可以得出以下思路:

我们可以在then方法执行时先对两个传递进来的回调函数进行保存,当状态改变之后对保存的回调函数以此进行调用

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class myPromise {
    #state = PENDING;
    #result = undefined;
    // 用于保存then方法中的回调函数等信息,then方法可能会被调用多次
    // 所以回调函数也可能会是多个所以这里声明一个数组
    #handlers = []; 
    constructor(executor) {
        const resolve = (data) => {
            this.#changeState(FULFILLED, data)
        };
        const reject = (err) => {
            this.#changeState(REJECTED, err)
        };
        try {
            executor(resolve, reject)
        } catch (err) {
            this.#changeState(REJECTED, err)
        }
    }
    // 因为这个方法是改变promise状态的,所以then方法中的逻辑也应该在这里执行一次
    #changeState(state, result) {
        if (this.#state != PENDING) return;
        this.#state = state;
        this.#result = result
        console.log(this.#state, this.#result)
        this.#run()
    }
    // 直接封装一个方法来解决延迟改变状态的问题
    #run(){
        // 延迟修改状态后then方法执行时此时状态为pending,不需要处理
        if(this.#state == PENDING) return;
        // 依次对存储的回调函数进行执行
        while(this.#handlers.length){
            const {onFulfilled,onReject} = this.#handlers.shift();
            if (this.#state == FULFILLED) {
                // 判断传递的是否是一个函数
                if (typeof onFulfilled == 'function') {
                    onFulfilled(this.#result)
                }
            } else if (this.#state == REJECTED) {
                if (typeof onRejected == 'function') {
                    onRejected(this.#result)
                }
            }
        }
    }
    
    then(onFulfilled, onRejected) {
        return new myPromise((resolve, reject) => {
            this.#handlers.push({
                onFulfilled,
                onRejected,
                // 后面第二个问题会讲到
                resolve,
                reject
            })
            // 这里也需要执行,如果状态没有状态被延迟修改则直接执行对应的回调函数
            this.#run();
        })
    }
}

// demo
const p = new myPromise((resolve, reject) => {
    setTimeout(() => {
        resolve(123)
    }, 1000)
})

p.then((res) => {
    console.log('res1', res)
}, (err) => {
    console.log('err1', err)
})
p.then((res) => {
    console.log('res2', res)
}, (err) => {
    console.log('err2', err)
})
p.then((res) => {
    console.log('res3', res)
}, (err) => {
    console.log('err3', err)
})
// res1 123
// res2 123
// res3 123

到此为止第一个问题就已经被解决了,下面看第二个问题,返回的promise状态该如何确定呢?

这个问题也分为三种情况:

1、传递对应的回调函数不是函数;

p.then(null,(err)=>{})

// 原生promise中
const p = new Promise((resolve,reject)=>{
    resolve(123);
})
p.then(null,()=>{}).then(res=>{
    console.log("res",res); // res 123
})

这种情况在原生promise中会进行穿透,即找到成功或失败对应的回调函数才会停止向后传递信息。

    #run(){
        if(this.#state == PENDING) return;
        while(this.#handlers.length){
            const {onFulfilled,onReject} = this.#handlers.shift();
            if (this.#state == FULFILLED) {
                if (typeof onFulfilled == 'function') {
                    onFulfilled(this.#result)
                }else{ // 传递的回调函数不是函数时
                    // 若不是函数传递的还是第一个promise的结果
                    // 会跟当前的promise状态保持一致
                    resolve(this.#result); 
                }
            } else if (this.#state == REJECTED) {
                if (typeof onRejected == 'function') {
                    onRejected(this.#result)
                }else{
                    reject(this.#result)
                }
            }
        }
    }
    
  // demo
  const p = new myPromise((resolve, reject) => {
        resolve(123)
  })
  p.then(null, (err) => {
    console.log('err', err)
  }).then(res=>{
    console.log("res",res)
  })

至此就已经解决了这种情况。

2、传递的回调函数是函数,是函数的话就需要看这个函数跟返回的promise的状态有什么关系,即函数执行没有报错的话就是resolve,有报错就是rejected,所以要给函数的运行加上try catch

   #run(){
        if(this.#state == PENDING) return;
        while(this.#handlers.length){
            const {onFulfilled,onReject} = this.#handlers.shift();
            if (this.#state == FULFILLED) {
                if (typeof onFulfilled == 'function') {
                    try{
                      const data = onFulfilled(this.#result)
                      resolve(data)
                    }catch(err){
                      reject(data)
                    }
                 }else{ // 传递的回调函数不是函数时
                    // 若不是函数,则传递的还是第一个promise的结果,
                    // 会跟当前的promise状态保持一致
                    resolve(this.#result); 
                }
            } else if (this.#state == REJECTED) {
                if (typeof onRejected == 'function') {
                    try{
                      const data = onRejected(this.#result)
                      resolve(data)
                   }catch(err){
                     reject(err)
                   }
                }else{
                    reject(this.#result)
                }
            }
        }
    }
    
    // 优化一下代码 
    // 添加一个新的方法
    #runOne(callback,resolve,reject){
        if(typeof callback != 'function'){
            const sattled = this.#state == FULFILLED ? resolve :reject;
            sattled(this.#result);
            return;
        }
        try{
            const data = callback(this.#result);
            resolve(data);
        }catch(err){
            reject(err)
        }
    }
     #run(){
        if(this.#state == PENDING) return;
        while(this.#handlers.length){
            const {onFulfilled,onReject} = this.#handlers.shift();
            if (this.#state == FULFILLED) {
                this.#runOne(onFulfilled,resolve,reject);
            } else if (this.#state == REJECTED) {
               this.#runOne(onRejected,resolve,reject);
            }
        }
    }

3、函数返回的结果是一个promise对象

  // 判断一个对象是否为promise对象
  isPromise(obj){
      return obj && typeof obj == 'object' && typeof obj.then == 'function';
  }
  #runOne(callback,resolve,reject){
        if(typeof callback != 'function'){
            const sattled = this.#state == FULFILLED ? resolve :reject;
            sattled(this.#result);
            return;
        }
        try{
            const data = callback(this.#result);
            if(this.isPromise(data)){
                // 如果then方法返回的是一个promise对象
                // 就把当前then方法返回的promise中的resolve和reject传进去
                // 来确认当前then方法返回的promise的状态
                data.then(resolve,reject)
            }else{
                resolve(data)
            }
        }catch(err){
            reject(err)
        }
    }

最后一个问题then里面的函数是运行在微队列中的:

  #runMicroTask(func){
      // 使用setTimeout模拟
      setTimeout(func,0)
  }
  #runOne(callback,resolve,reject){
      this.#runMicroTask(()=>{
        if(typeof callback != 'function'){
            const sattled = this.#state == FULFILLED ? resolve :reject;
            sattled(this.#result);
            return;
        }
        try{
            const data = callback(this.#result);
            if(this.isPromise(data)){
                data.then(resolve,reject)
            }else{
                resolve(data)
            }
        }catch(err){
            reject(err)
        }
      });
    }

五、手写深拷贝

浅拷贝:如果是基本数据类型的值则直接拷贝基本数据类型的值,如果是引用类型的值则拷贝的是一个堆内存对象中的引用地址,即一个值的变化会影响另一个值的变化。Object.assign()、slice()、concat()等。

深拷贝:一个值的变化不会影响另一个值的变化。JSON.stringify()、jquery.extend()、lodash._cloneDeep()、手写递归,下面主要演示手写递归的方法。

要实现深拷贝必不可少的是需要判断数据的类型,所以可以单独抽离出一个方法专门用来判断数据的类型:

function checkType(data) {
    return Object.prototype.toString.call(data).slice(8, -1);
}

之所以要进行截取是因为Object.prototype.toString.call()方法返回的是[object Object]格式,我们只需要最后的类型即可。下面来实现深拷贝函数。

function deepClone(target) {
    let type = checkType(target);
    let result;
    if (type == 'Object') {
        result = {};
    } else if (type == 'Array') {
        result = [];
    } else {
        return target;
    }
    for (let key in target) {
        let value = target[key];
        let valueType = checkType(value);
        // 如果属性值依然为引用类型则再次执行该方法
        if (valueType == 'Object' || valueType == 'Array') {
            result[key] = deepClone(value);
        } else {
            // 直到为基本类型为止
            result[key] = value;
        }
    }
    return result;
}

首先要对传入的数据进行类型的判断,如果是基本数据类型不需要进行深拷贝直接返回,如果是引用数据类型则针对不同的数据类型作初始化操作。紧接着对传递的数据进行遍历,属性值可能也会是引用类型所以需要进行递归,直至全部为基本数据类型的时候再进行整体返回。

let obj1 = {
    name: "小明",
    age: 20,
    hobby: ['sing','dance']
}
let obj2 = deepClone(obj1)
obj2.hobby[0] = 'sleep'
console.log(obj1, obj2) 
// { name: '小明', age: 20, hobby: [ 'sing', 'dance' ] }
// { name: '小明', age: 20, hobby: [ 'sleep', 'dance' ] }

六、手写防抖函数

函数防抖是指在一定时间内,多次触发事件只执行最后一次事件处理函数。

例如:搜索框输入事件、鼠标移入移出事件

const box = document.querySelector('#box');
box.onmousemove = debounce(() => {
    console.log('mousemove');
}, 1000)

function debounce(fn,delay){
    let timer = null;
    return function(){
        if(timer) clearTimeout(timer)
        timer = setTimeout(()=>{
            fn()
        },delay)
    }
}

手写节流函数

连续发生的事件在 n 秒内只执行一次,限制函数在一定时间内的执行次数.

function throttle(fun, delay) {
  let timeout;
  return function () {
    if (!timeout) {
      timeout = setTimeout(() => {
        timeout = null;
        fun();
      }, delay);
    }
  };
}

持续更新ing。。。。。。

时间就像海绵。