手写代码

345 阅读6分钟

前言

面试复习总结:多参照于冴羽的博客

1. 实现一个new操作符

  • 首先函数接受不定量的参数,第一个参数为构造函数,接下来的参数被构造函数使用
  • 然后内部创建一个空对象 obj
  • 因为 obj 对象需要访问到构造函数原型链上的属性,所以我们通过 obj.proto = func.prototype 将两者联系起来。
  • 通过call或apply,把构造函数的this指向obj
  • 判断构造函数返回值是否为对象,如果为对象就使用构造函数返回的值,否则使用 obj,这样就实现了忽略构造函数返回的原始值
function Person(name, age) {
    this.name = name
    this.age = age
}
Person.prototype.add = function() {
    console.log(this.name)
    console.log(this.age)
}
function objectFactory(func) {
    var obj = {}
    var arg = [...arguments].slice(1)
    obj.__proto__ = func.prototype;
    var res = func.apply(obj, arg)
    if ((typeof res === 'Object' || typeof res === 'Function') && res !== null) {
        return res 
    }
    return obj
}
var person = objectFactory(Person, 'zh', 18)

2.实现一个call apply 或 bind

2.1 call

fun.call(thisArg, arg1, arg2, ...)

let obj = {
    value: 1
}
function bar(name, age) {
    console.log(this.value)
    console.log(name)
    console.log(age)
}
bar.call(obj, 'zh', 18)

注意:

  1. call改变了函数this指向, 指向bar
  2. 除了第一个参数, 其余参数一个个传入
  3. bar函数执行

可通过下面模拟第1,2步

  1. 将函数设为对象的属性
  2. 执行该函数
  3. 删除该函数
obj.fn = bar
obj.fn()
delete obj.fn()

完整版

Function.prototype.myCall = function(context) {
    context = context || window
    let args = [...arguments].slice(1)
    context.fn = this
    let res = context.fn(...args)
    delete context.fn
    return res
}
bar.myCall(obj, 'zh', 18)

2.2 apply

Function.prototype.myApply = function(context) {
    let res
    context = context || window
    context.fn = this
    if (arguments[1]) {
        context.fn(args)
    } else {
        context.fn()
    }
    delete context.fn
    return res
}
bar.myApply(obj, ['zh', 18])

2.3 bind

bind() 方法会创建一个新函数。当这个新函数被调用时,bind()的第一个参数将作为它运行时的 this,而其余参数将作为新函数的参数,供调用时使用
bind 函数算是手写部分比较难的一类,因为要考虑到绑定后返回的函数还能够作为构造函数被实例化

Function.prototype.myBind = function(context) {
    if(typeof this != "function") {
        throw Error("not a function")
    }
    let fn = this
    let args = [...arguments].slice(1)
    let res =  function() {
        //如果是构造函数,this (barbind 实例)指向 res(new barBind 构造函数), 此时结果为 true, 将绑定函数的 this 指向 this,
        //如果是普通函数,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return fn.apply(this instanceof res ? this : context,args.concat(...arguments) )
    }
 // res.prototype = this.prototype 修改函数原型时,构造函数的原型也会被同步修改了,可通过一个空函数来进行中转
    function temp() {}
    temp.prototype = this.prototype 
    res.prototype = new temp()
    return res
}
var obj = {
    value: 1
}
function Bar(name,age) {
   console.log(this.value)
   console.log(name, age)
   this.name = name
   this.age = age
}
Bar.prototype.add = function() {
    console.log(this.name)
}
var barBind = Bar.myBind(obj)
var barbind = new barBind('zh', 18)

3. 手写防抖(Debouncing)和节流(Throttling)

3.1 防抖

事件持续触发,等到事件停止触发后n秒才去执行函数,如果n秒内又触发了该事件,那我就以新的事件的时间为准,n 秒后才执行。

function debounce(func, delay) {
    let timeout;
    return function () {
        const context = this;
        const args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, delay);
    }
}

**新增需求:
1、事件触发,立刻执行函数,然后等到停止触发后 n 秒才执行函数。
2、事件再次触发,等到事件停止触发后n秒才去执行函数,
3、增加immediate 参数判断是否是立刻执行
function debounce(fn, delay, immediate=false){
    let timeout = null;
    function debounce(fn, delay, immediate = false) {
      let timeout = null;
      return function (args) {
        let context = this;
        const args = arguments;
        if (timeout) clearTimeout(timeout)
        if (immediate) { // 第一次触发时,timeout为null,立即执行一次回调
          fn.apply(context, args);
          immediate = false
        } else {
          timeout = setTimeout(function () {
            fn.apply(context, args);
          }, delay)
        }
      }
    }
}

**新增需求:
1、事件触发,立刻执行函数。然后等到停止触发后n秒,才能重新触发事件
2、增加immediate 参数判断是否是立刻执行
function debounce(func, wait, immediate=flase) {
    var timeout;
    return function () {
        var context = this;
        var args = arguments;
        if (timeout) clearTimeout(timeout); 
        if (immediate) {
            if (!timeout) func.apply(context, args)  //**clearTimeout(timeout) 清掉的是计时器任务, 而timeout的值还在
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

**新增需求:
1、事件触发,立刻执行函数,然后等到停止触发后 n 秒才执行函数。
2、事件再次触发,立刻执行函数,然后等到停止触发后 n 秒才执行函数。
3、增加immediate 参数判断是否是立刻执行
function debounce(func, delay, immediate) {
      var timeout
      return function() {
        var context = this;
        var args = arguments;
        if (timeout) clearTimeout(timeout)
        if (immediate) {
          if (!timeout) func.apply(context, args)
          timeout = setTimeout(function () {
            func.apply(context, args)
            timeout = null
          }, delay)
        } else {
          timeout = setTimeout(function () {
            func.apply(context, args)
          }, delay)
        }
      }
    } 
}

**最终版本
function debounce(func, wait, immediate) {
    var timeout, result;
    var debounced = function () {
        var context = this;
        var args = arguments;
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            //当immediate=true 能返回函数的执行结果
            //当immediate=false setTimeout的返回值是id值,不能返回函数的执行结果
            if (!timeout) result = func.apply(context, args) 。
            timeout = setTimeout(function () {
                func.apply()
                timeout = null
            }, delay)
        }else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
        return result;
    };
    debounced.cancel = function() { //取消 debounce 函数
        clearTimeout(timeout);
        timeout = null;
    };
    return debounced;
}

函数防抖的应用场景
连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

3.2 节流

节流是持续触发的时候,每 n 秒执行一次函数
有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。

**时间戳
function throttle(func, wait) {
    var context, args;
    var previous = 0;

    return function() {
        var now = +new Date();
        context = this;
        args = arguments;
        if (now - previous > wait) { 立刻执行第一次
            func.apply(context, args);
            previous = now;
        }
    }
}

**定时器
function throttle(func, wait) {
    var timeout;

    return function() {
        context = this;
        args = arguments;
        if (!timeout) {
            timeout = setTimeout(function(){ n 秒后第一次执行
                timeout = null;
                func.apply(context, args)
            }, wait)
        }

    }
}
两者区别:  
- 第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行  
- 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件 

**新增需求:事件触发函数立刻执行,停止触发的时候还执行函数一次
function throttle(func, wait=1000) {
    var timeout, context, args, result;
    var previous = 0;
    var throttled = function() {
        context = this;
        args = arguments;
        var now = +new Date();
        var remaining = wait - (now - previous);
        // 第一次: now-previouse>1000 remaining<0 立刻执行函数
        // 第二次: now-previouse<=1000  当remaining=1000&&!timeout  1000秒后执行函数, 如此反复
        // 离去:  1、 now-previouse<1000  0<remaining<=1000 1000秒后执行函数,
        //         2、 now-previouse>1000  remaining<0 立刻执行函数
        if (remaining <= 0 || remaining > wait) { // 如果没有剩余的时间了或者你改了系统时间
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            func.apply(context, args);
            previous = now;
        } else if (!timeout) {
            timeout = setTimeout(function() {
                previous = +new Date();
                func.apply(context, args)
                timeout = null;
            }, remaining);
        }
    };
    return throttled;
}


**优化:有时也希望无头有尾,或者有头无尾, 或是有头有尾
1、设置个 options 作为第三个参数
2、leading:false 表示禁用第一次执行
3、trailing: false 表示禁用停止触发的回调
function throttle(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};
    
    var throttled = function() {
        context = this;
        args = arguments;
        var now = +new Date();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            func.apply(context, args);
            previous = now;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(function() {
                func.apply(context, args);
                previous = +new Date();
                timeout = null;
            }, remaining);
        }
    };
    throttled.cancel = function() {
        clearTimeout(timeout);
        previous = 0;
        timeout = null;
    }
    return throttled;
}

函数节流的应用场景
间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 谷歌搜索框,搜索联想功能
  • 高频点击提交,表单重复提交