前端手写代码

2020-07-22  本文已影响0人  书虫和泰迪熊

下面介绍一些常用的源码实现
实现一个深拷贝
实现 new 操作符
实现instanceof
防抖
节流
函数柯里化
实现 call,apply
实现 bind
实现Ajax
Promise.all

实现一个深拷贝

深拷贝为对象创建一个相同的副本,两者的引用地址不同。
一般方法:JSON的序列化和反序列化(JSON.Parse(JSON.stringify(obj))),但是这种方式有两个不足:
1.undefined, null, symbol 类型的值会被删除。
2.碰见循环引用的时候回报错
循环引用解决方案:可以使用一个WeakMap结构存储已经被拷贝的对象,每一次进行拷贝的时候就先向WeakMap查询该对象是否已经被拷贝,如果已经被拷贝则取出该对象并返回.

下面我们简单实现一个深拷贝

function isObj(obj) {
    return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}
function deepCopy(obj, hash = new WeakMap()) {
    if(hash.has(obj)) return hash.get(obj)
    let cloneObj = Array.isArray(obj) ? [] : {}
    hash.set(obj, cloneObj)
    for (let key in obj) {
        cloneObj[key] = isObj(obj[key]) ? deepCopy(obj[key], hash) : obj[key];
    }
    return cloneObj
}
// 测试
let o1 = {
    a: 1,
    b: null,
    c: undefined,
    d: "hello",
    e: [111,222,333,444],
    f: true,
    s: Symbol('ww'),
}
o1.oo = o1   // 循环引用
let o2 = deepCopy(o1)
console.log(o2)

实现 new 操作符

大体可以分为3步
1.创建一个新的对象
2.将新对象的的 _ proto _ 指向 构造函数的 prototype 对象
3.执行构造函数,并将 this 指向新的对象

function myNew(fn, ...args) {
    if(typeof fn !== 'function') {
        throw fn + 'is not a constructor'
    }
    let obj = {};
    obj.__proto__ = fn.prototype;
    // let obj = Object.create(null)  也使用 ES5 的 Object.create() 来代替上面两行代码
    let res = fn.apply(obj, args);
    return res instanceof Object ? res : obj;  
    // return 的时候需要对返回的东西进行判断,若是对象则返回,如果不是对象,则返回新创建的对象。
}
// 测试
function Person(name, age) {
    this.name = name;
    this.age = age
}
console.log(myNew(Person, "dingding", 100))

实现instanceof

a instanceof b 用于判断 构造函数 b 的 prototype 是否存在于 a 的原型链上
常用于判断引用类型

function myInstanceof(left, right) {
    left = left.__proto__;
    
    while(left !== right.prototype) {
        left = left.__proto__
        if(left === null) 
        return false
    }
    return true;
}


// 测试
var a = []
var b = {}
 
function Foo(){}
var c = new Foo()
 
function child(){}
function father(){}
child.prototype = new father() 
var d = new child()
 
console.log(instance_of(a, Array)) // true
console.log(instance_of(b, Object)) // true
console.log(instance_of(b, Array)) // false
console.log(instance_of(a, Object)) // true
console.log(instance_of(c, Foo)) // true
console.log(instance_of(d, child)) // true

防抖

在事件被触发n秒后再执行回调函数,如果在这n秒内又被触发,则重新计时

function ajax() {
    console.log("我是 ajax")
}
function debounce(cb, delay) {
    let timer = null;
    return function(args) {
        let that = this;   // 获得函数的作用域
        clearTimeout(timer);  // 每次事件被触发,都会清除当前的timeer,然后重写设置超时调用
        timer = setTimeout(function(){
            cb.apply(that, args);
        }, delay)
    }
}

// 测试,模拟一个在 2100毫秒内每隔半秒就重复触发的事件
var rsu = debounce(ajax, 1000,);
let itv = setInterval(()=> {
    rsu(888)
}, 500)
setTimeout(()=> {
    clearInterval(itv)
}, 2100)

节流

使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数

function jl(fn, time, ...args) {
    let lock = false;
    return function() {
        console.log(1111)
        if(lock) return;
        setTimeout(() => {
            fn.apply(this, ...args);
            lock = true;
        }, time)
    }
}
function ajax() {
    console.log(999999999)
}
// 测试
var fun = jl(ajax, 2000);
let st = setInterval(()=> {
    fun()
}, 600)
setTimeout(()=> {
    clearInterval(st)
}, 2000)

函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

函数柯里化

实现 sum(11, 22, 33) => sum(11, 22)(33)() = 66 的效果
传入参数时,不执行,而是先记忆起来,延迟计算,什么时候想要计算,直接 sum() 就行

var currying = function (fn) {
    var args = []
    return function() {
        if(arguments.length === 0) {
            return fn.apply(this, args)
        }
        // [].slice.call(arguments) 将函数的实际参数转化成数组
        Array.prototype.push.apply(args, [].slice.call(arguments))
        // arguments.callee 返回当前匿名函数
        // rguments 的主要用途是保存函数参数, 但这个对象还有一个名叫 callee 的属性,
        // 返回正被执行的 Function 对象,也就是所指定的 Function 对象的正文,这有利于匿名函数的递归或者保证函数的封装性
        return arguments.callee
    }
}
var tempFun = function() {
    var sum = 0; 
    for(var i = 0; i < arguments.length; i++) {
        sum += arguments[i]
    }
    return sum
}
// 测试
var sum = currying(tempFun);
sum(11,22)
sum(33)
sum()

call 和 apply

用法:https://www.jianshu.com/p/aa2eeecd8b4f

原本操作

var o1 = {
 name: 'dingding'
}
function say() {
    console.log(222, this.name)
}
o1.fun = say;
o1.fun()     // 222 dingding

如果不使用 o1.fun = say 进项绑定
手写apply

Function.prototype.myapply = function(context, [...args]) {
  context = context || window;   // 判断上下文是否为空,若为空则指向window 对象
  context.fn = this;      
  context.fn(...args);
  delete context.fn;      // 要删除fn,不能影响原对象
}

// 测试
var o1 = {
 name: 'dingding'
}
function say() {
    console.log(222, this.name)
}
say.apply(o1)

手写call

Function.mycall = function(context) {
  context = context || window;
  let args = [...arguments].slice(1);   // 截取arguments,从下标1开始的
  context.fn = this;
  context.fn(args);
  delete context.fn;
}

bind

bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()第一个参数的值,
例如,f.bind(obj),实际上可以理解为obj.f(),这时,f函数体内的this自然指向的是obj
bind 基础用法:https://www.jianshu.com/p/25a855c01896

下面实现myBind 方法可以绑定对象,可以传参, 还要注意函数柯里化的情况

Function.prototype.myBind = function(context) {
    const self = this;
    let args = [...arguments].slice(1);   // args: [7, 8]
    
    return function() {
        // 考虑函数柯里化的情况
        let newArgs = [...arguments];     //newArgs: [9]
        return self.apply(context, newArgs.concat(args))
    }
}

// 测试
function a(m, n, o){
    return this.name + ' ' + m + ' ' + n + ' ' + o;
}
var b = {name : 'kong'};
console.log(a.myBind(b, 7, 8)(9)); 

实现Ajax

实现一个ajax其实主要是一个XMLHttpRequest对象以及其API方法的一个使用的问题。而在这里我建议尽量封装成promise的形式,方便使用。

function ajax({url, methods, body, headers}) {
    return new Promise((resolve, reject) => {
        let request = new XMLHttpRequest();
        request.open(url, methods);
        for(let key in headers) {
            let value = headers[key]
            request.setRequestHeader(key, value);
        }
        request.send(body)
        request.onreadystatechange = () => {
            if(request.readyState === 4) {
                if(request.status >= '200' && request.status < 300) {
                    resolve(request.responeText);
                } else {
                    reject(request)
                }
            }
        }
       
    })
}

Promise.all

// 假设一下Promise其他所有函数都正常工作,但Promise.all功能失效了,我们现在就要为程序重写一个Promise.all
Promise.all = function(promises) {
    let results = [];
    let promiseCount = 0;
    return new Promise((resolve, reject) => { 
        for(let i = 0; i < promises.length; i++) {            // 使用let保证promise顺序执行
            Promise.resolve(promises[i]).then(res => {    // 传入的 promises 元素可能不是 Promise 类型的,使用 Promise.resolve(arr[i]) 转换。
                promiseCount++;
                results[i] = res;
                if(promiseCount === promises.length) {        // 当所有函数都正确执行了,resolve输出所有返回结果。
                    resolve(results);
                }
            }, err => {
                reject(err);
            })
        }
    })
}

let p1 = new Promise((resolve) => {
    setTimeout(()=> {
        console.log("p1 resolve");
        resolve(111);
    }, 1000)
})
let p2 = new Promise((resolve) => {
    console.log('p2 resolve');
    resolve(222);
})
let p3 = new Promise((resolve) => {
    console.log('p3 resolve');
    resolve(333);
})

var p = Promise.all([p1, p2, p3]);
console.log(1212, p)
p.then(e => {
    console.log(e)
});



若有错误,欢迎留言~~

image.png
上一篇下一篇

猜你喜欢

热点阅读