✍🏻️什么是手写代码!让我们来一起研究!| 持续更新...

227 阅读5分钟

前言

虽然开始写这些手写代码的初衷,也是为了面试而准备,但是这也是一个检验自己基础扎不扎实的好机会。

话不多说,让我们一起冲冲冲!

1. new的实现

function New(fn, ...args) {
	const obj = Object.create(fn.prototype);
  const res = fn.call(obj, ...args);
  return res instanceof Object ? res : obj; 
}

测试代码:

function Person(name, age) {
	this.name = name;
  this.age = age;
}
const instance1 = New(Person, '老王', 22);
const instance2 = New(Person, '老肖', 28); 
console.log(instance1); // Person {name: "老王", age: 22}
console.log(instance2); // Person {name: "老肖", age: 28}
console.log(instance1.__proto__ === Person.prototype); // true
console.log(instance2.__proto__ === Person.prototype); // true
console.log(instance1 instanceof Person); // true
console.log(instance2 instanceof Person); // true

2. 事件总线(发布订阅)

ES5实现:

  • 通过立执行函数,创建一个独立命名空间
  • 这个空间主要用于存储EventEimtter类的私有属性和方法
// 立即调用函数 
var EventEimtter = (function (params) {
    // 私有属性:_map
    var _map = Object.create(null);
    function EventEimtter() {}
    // 私有变量:_on
    function _on(eventName, fn, once) {
        if (_map[eventName]) {
            _map[eventName].fn.push(fn)
        } else {
            _map[eventName] = { fn: [fn], once: once };
        }
    }
    EventEimtter.prototype.on = function (eventName, fn) {
        _on(eventName, fn, false);
    }
    
    EventEimtter.prototype.once = function (eventName, fn) {
        _on(eventName, fn, true);
    }
    
    EventEimtter.prototype.off = function (eventName, fn) {
        delete _map[eventName];
    }
    
    EventEimtter.prototype.emit = function (eventName) {
        var args = Array.prototype.slice.call(arguments, 1);
        var target = _map[eventName]
        if (!_map[eventName]) {
            return false;
        }
        var fn = target.fn || [];
        for (var i = 0; i < fn.length; i++) {
            fn[i].apply(null, args);
        }
        if (target.once) {
            delete _map[eventName];
        }
        return true;
    }
    return EventEimtter;
})();

ES6实现:

const _map = Symbol('map');
const _on = Symbol('on');
class EventEimtter {
    // 用下划线暂时区别的类的公用方法和私有方法
    [_on](eventName, fn, once) {
        if (this[_map].has(eventName)) {
            this[_map].get(eventName).fn.push(fn);
        } else {
            this[_map].set(eventName, { fn: [fn], once })
        }
    }
    constructor() {
        this[_map] = new Map();
    }
    on(eventName, fn) {
        this[_on](eventName, fn, false);
    }
    once(eventName, fn) {
        this[_on](eventName, fn, true);
    }
    off(eventName) {
        this[_map].delete(eventName);
    }
    emit(eventName, ...args) {
        const { fn, once } = this[_map].get(eventName) || {};
        // 无对应的订阅回调事件,返回false
        if (!fn) {
            return false;
        }
        // 依次执行回调函数
        for (let i = 0; i < fn.length; i++) {
            fn[i](...args);
        }
        if (once) {
            this.off(eventName);
        }
        return true;
    }
}

测试代码:

const events = new EventEimtter();
// 订阅event1事件
events.on('event1', (a, b) => {
    console.log(a + b);
});
// 订阅event1事件
events.on('event1', (a, b) => {
    console.log(a * b);
});
// 订阅onceEvent事件
events.once('onceEvent', (a, b) => {
    console.log(a + b);
});
// 发布event1事件和onceEvent事件
events.emit('event1', 2 , 3);
events.emit('onceEvent', 2 , 2);

// 取消订阅 event1 event1
events.off('event1');
events.off('onceEvent');

// 返回false
console.log(events.emit('event1'));
console.log(events.emit('onceEvent'));

3. call的实现

call的第一个参数传入null或者undefined,this就会指向全局对象或者undefined。

Function.prototype.myCall = function (context, ...args) {
    const fn = this;
    // context为null或者为undefined
    // 直接执行,函数this指向的就是当前全局对象 或者 undefined
    if (context === null || context === undefined) {
        return fn(...args);
    }
    const _context = Object.assign({}, context);
    _context.fn = fn;
    return _context.fn(...args);
}

测试代码:

var name = 'global name';
function test(a, b) {
    console.log(`${this.name} a:${a} b:${b}`);
}
test.myCall(null, 1, 2);
test.myCall(1, 1, 2);
test.myCall({ name: 'obj name' }, 1, 2);

4. instanceof的实现

instanceof的原理主要依赖原型链,所以判断目标对象的原型链上是否包含构造函数的原型对象即可。 另外对于原始值,instanceof 一律返回false;

function myInstanceof (target, constructor) {
    // 判断构造函数的类型和对应的原型对象是否存在
    if (typeof constructor !== 'function') {
        throw(new TypeError('参数2应该是一个构造函数'))
    }
  	if (!constructor.prototype) {
        throw(new TypeError('error msg'));
    }
 	  // 处理原始值,返回false
    if (['string', 'number', 'boolean', 'symbol', 'bigint', 'undefined'].indexOf(typeof target) > -1 
    || target === null) {
        return false;
    }
    const prototype = constructor.prototype;
    while (target) {
        target = Object.getPrototypeOf(target);
        if (target === prototype) {
            return true;
        }
    }
    return false;
}

测试代码:

class A {
    constructor(){
        this.a = 1;
    }
}
const a = new A();
console.log(myInstanceof(a, A));
console.log(a instanceof A);

console.log(myInstanceof({}, Object));
console.log({} instanceof Object);

console.log(myInstanceof([], Array));
console.log([] instanceof Array);

console.log(myInstanceof(1, Number));
console.log(1 instanceof Number);

5. 防抖

对于一些频繁触发的动作,在动作结束后一段时间后,触发回调函数。比如实时搜索的场景,减少ajax请求次数,在输入完整的关键字后才开始请求资源。

  • fn: 回调函数
  • t: 延时
  • leading: 是否在延时开始前执行一次
function debounce(fn, t = 500, leading = false) {
    let timer;
    let lastInvokeTime;
    function reset () {
        lastInvokeTime = undefined;
        timer = undefined;
    }
    return function (...args) {
        const context = this;
        if (leading && lastInvokeTime === undefined && timer === undefined) {
            fn.apply(context, args);
            lastInvokeTime = Date.now();
        }
        if(timer) {
            timer = clearTimeout(timer);
        }
        timer = setTimeout(() => {
            fn.apply(context, args)
            reset()
        }, t);
    }

}

测试代码:

window.onload  = function() {
    var inputchange = debounce(function(e) { console.log(this, e) }, 1000, true);
    const input = document.querySelector('input');
    input.addEventListener('input', inputchange);
}

5. 节流

也是针对频繁发生的操作,设定一个时间周期,在这段时间内只能执行一次操作的回调函数。比如在监听页面滚动的时候,需要实时触发dom的更新,此时需要限制这种更新的频率就需要节流。

  • fn: 回调函数
  • t: 延时
  • leading: 是否在延时开始前执行一次
function throttle(fn, t = 500, leading = true) {
    let previous;
    return function(...args) {
        const now = new Date().valueOf();
        const context = this;
        if (leading && previous === undefined) {
            fn.apply(context, args);
            previous = now;
        }
        if (previous && now - previous > t) {
            fn.apply(context, args);
            previous = now;
        } else if (previous === undefined) {
            previous = now;
        }
    }
}
  1. 函数柯里化 函数柯里化的概念就是将多个传参的函数,转化成单个参数的函数。 比如以下函数add,计算1+2+3时:

未柯里化之前: add(add(1, 2), 3)

柯里化之后: curryAdd(1)(2)(3)

ES5:

function curry(fn, args) {
    var len = fn.length;
    args = args || [];
    return function () {
        var newArgs = args.concat(Array.prototype.slice.call(arguments))
        if (newArgs.length < len) {
            return curry.call(this, fn, newArgs);
        } else if (newArgs.length === len) {
            return fn.apply(this, newArgs);
        }
    }
    
}

ES6:

function curry(fn, args = []) {
    return (...extraArgs) => {
        const newArgs = [...args, ...extraArgs]
        return newArgs.length === fn.length ? fn(...newArgs) : curry(fn, newArgs)
    }
}

测试代码:

function add (a, b, c) {
	return a + b + c;
}
var curryAdd = curry(add);
curryAdd(1)(2)(3)

7. 深拷贝

⚡️考察点

  • 递归
  • 不同类型数据的处理
// 判断是否为原始值
function isOriginalValue(val) {
    return ['string', 'number', 'boolean', 'symbol', 'bigint', 'undefined'].indexOf(typeof val) > -1 || val === null;
}

function isDate(obj) {
    return Object.prototype.toString.call(obj) === '[object Date]';
}

function isRegExp(obj) {
    return Object.prototype.toString.call(obj) === '[object RegExp]';
}

function isSet(obj) {
    return Object.prototype.toString.call(obj) === '[object Set]'
}

function isMap(obj) {
    return Object.prototype.toString.call(obj) === '[object Map]'
}


// 生成初始对象
function getInitTarget(obj) {
    const C = obj.constructor;
    return new C();
}
// 获取可遍历对象的key集合
function getKeys(obj) {
    const type = Object.prototype.toString.call(obj);
    let keys = [];
    switch (type) {
        case '[object Object]': {
            keys = Object.keys(obj);
            break;
        }
        case '[object Array]': {
            keys = Array.from({ length: obj.length }, (val, index) => index);
            break;
        }
        case '[object Set]':
        case '[object Map]': {
            keys = [...obj.keys()];
            break;
        }
    }
    return keys;
}


function cloneDeep(obj, map = new WeakMap()) {
    // 原始值 直接返回
    if (isOriginalValue(obj)) return obj;
    // 防止循环引用 判断对象有没有被遍历过
    if (map.get(obj)) return map.get(obj);
    map.set(obj, obj);
    const targetObj = getInitTarget(obj);
    const keys = getKeys(obj);
    let index = -1;
    while(++index < keys.length) {
        const key = keys[index];
        const val = (isSet(obj) || isMap(obj)) ? obj.get(key) : obj[key];
        if (isOriginalValue(val) || typeof obj === 'function') {
            if ((isSet(obj) || isMap(obj))) {
                targetObj.set(key, val);
            } else {
                targetObj[key] = val;
            }
        } else if (isDate(obj)) {
            targetObj[key] = new Date(obj.valueOf());
        } else if (isRegExp(obj)) {
            targetObj[key] = new RegExp(obj.valueOf());
        } else {
            // 其他引用对象处理
            targetObj[key] = cloneDeep(val, map);
        }
    }
    return targetObj;
}

测试代码:

let a = { a1: 1, a2: [1, 2, 3] , a3: {b: 1, c: { d: 2 }}, a4: new Map([[1, 4], [2, 3]]), a5: new Date()};
a.a = a;
const b = cloneDeep(a);

8. 数组相关

8-1. 数组扁平化

ES5:

function flatDeep(arr, d = 1) {
  return d > 0 ? 
    arr.reduce((res, val) => res.concat(Object.prototype.toString.call(val) === '[object Array]' ? flatDeep(val, d-1) : val), [])
  :arr.slice()
}

flatDeep([1,2,[3, [4,5]]], Infinity); // [1, 2, 3, 4, 5]

ES6:

[1,2,[3, [4,5]]].flat(Infinity); // [1, 2, 3, 4, 5]

8-2. 类数组转化

常见的类数组:

  1. 函数auguments对象
  2. 获取到Dom节点,document.getElementByClassName("ls")
  3. 二进制数组

转化为数组的处理方式:

  1. Array.from()
  2. ...扩展运算符
  3. 利用数组的方法,返回一个新的数据(concat slice)
const likeArr = document.getElementsByTagName('li');
const arr1 = Array.from(likeArr);
const arr2 = [...likeArr];
const arr3 = Array.prototype.slice.call(likeArr);

8-3. 类数组去重

ES5:

var arr = [1, 1, 2, 2, 3, 4];
var targetArr = [];
for (var i = 0; i < arr.length; i++) {
    if (i === 0 || arr[i] !== arr[i - 1]) {
        targetArr.push(arr[i]);
    }
}

ES6:

const arr = [1, 1, 2, 2, 3, 4];
const uniArr1 = [...new Set(arr)];
const uniArr2 = Array.from(new Set(arr));

9. 类的继承

9-1.原型链继承

通过指定子类原型对象为父类的实例,来继承父类的实例及原型对象的属性及方法。

缺点:

  1. 由于所有子类都引用了父类的实例对象,当继承于父类的属性发生变更时,所以继承的子类实例都会被影响。
  2. Son.prototype.constructor !== Son,即Son类原型对象的构造函数不指向Son函数。
function Father() {
  this.name = 'father name';
  this.sonAges = [19,20,30];
}
Father.prototype.say = function() {
  console.log(this.name);
}

function Son() {
  this.name = 'son name';
}
Son.prototype = new Father();

const son1 = new Son();
const son2 = new Son();

// 指向同一个父类实例,修改引用类型会影响其他子类实例
son1.sonAges.push(40);
console.log(son1.sonAges);
console.log(son2.sonAges);
console.log(son1.__proto__.__proto__ === Father.prototype) // true
console.log(Son.prototype.constructor === Son) // false

9-2.通过构造函数继承

通过子类在构造函数中调用父类的构造函数。

优点: 避免了原型链继承时,引用对象被所有子类实例共享的情况。也可以向父类的构造函数中传参。

function Father(name) {
  this.name = name;
}
Father.prototype.say = function() {
  console.log(this.name);
}
function Son(name) {
	Father.call(this, name);
}
const son1 = new Son('小王');
const son2 = new Son('小肖');
console.log(son1.name); // 小王
console.log(son2.name); // 小肖

缺点:

image.png

从图中可以看到子类并未继承父类在原型对象上定义的say方法。因为这种继承方式只是简单的把父类的在构造函数中定义的属性和方法添加到子类的实例上,原型链上也并未实现。

son1.__proto__.__proto__ === Father.prototype // false

如果需要继承父类的方法,需要把这个方法定义在父类的构造函数中。

function Father(name) {
  this.name = name;
  this.sayInfun = function() {
  	console.log(this.name);
  }
}
console.log(son1.sayInfun()); // 小王

9-3.组合型继承

结合原型链继承和构造函数继承两种方式,弥补两种方式的缺点。但是这种方法也有自己的缺点,需要调用两次父类的构造函数。

function Father(name, age) {
  this.name = name;
  this.age = age;
}
function Son(name, age, job) {
	Father.call(this, name, age);
  this.job = job;
}
Son.prototype = new Father();

Son.prototype.constructor = Son;
let son = new Son('大王', 24, '工具人');
Son {name: "大王", age: 24, job: "工具人"}

9-4.寄生式继承

主要通过object.create方法,来生成一个以目标原型对象为原型的的对象,并在此基础上对生成的对象进行扩充。

function creatObj(pro_obj, name) {
	const obj = Object.create(pro_obj);
  obj.name = name;
  obj.say = function() {
  	console.log('hhh');
  }
  return obj;
}

测试:

var a = createObj({ b: 1, c(){console.log(111)}}, 'aaa');
// { name: "aaa", say: ƒ}

缺点: 原型对象上的方法也是无法继承,需要在方法中重新去创建方法。

9-5.寄生组合式继承

最优解继承。

function Father (name, age) {
  this.name = name;
  this.age = age;
}
function Son(name, age, job) {
	Father.call(this, name, age);
  this.job = job;
}

function extend(father, son) {
  // 减少一次调用父类构造函数
  function F() {};
  F.prototype = father.prototype;
  const f = new F();
  f.constructor = son;
  son.prototype = f;
}

extend(Father, Son);

const son1 = new Son('小王', 22, '打工人');
// son1: {name: "小王", age: 22, job: "打工人"}
console.log(son1 instanceof Father); // true;

10. Ajax的实现

参考axios源码,在使用XMLHttpRequest进行http请求的基础上,进行一层promise的封装。

function request(url, method, request) {
    return new Promise(function(resolve, resject) {
        const xhr = new XMLHttpRequest();
        xhr.open(method, url, true);
        // 请求状态更改
        xhr.onreadystatechange = function() {
            // readyState 0 = 未启动 1 = 调用open 2 = 调用send 3 = loading 4 = done
            if (!xhr || xhr.readyState !== 4) return;
            const response = xhr.response || xhr.responseText;
            if (xhr.status === 200) {
                resolve(response);
            } else {
                resject(response);
            }
            xhr = null;
        };
        // 请求错误
        xhr.onerror = function() {
            const errorMsg = 'Network Error: ' + url;
            resject(errorMsg);
        }
        xhr.ontimeout = function() {
            const errorMsg = 'Timeout: ' + url;
            resject(errorMsg);
        }
        xhr.send(request);
    });
}

11. 本地图片预览

11-1:通过Blob URL

一个指向存储在磁盘或者内存中blob的地址,来实现对这个blob的简单引用。 这个地址可以作为文件下载地址,也可以作为图片资源地址。

显示格式

blob:http://XXX

生成Blob URL

URL.createObjectURL()  方法会创建一个特殊的Blob或者File对象的引用。 同一个Blob 对象或者file对象每次通过 URL.createObjectURL() 会创建一个新的Blob URL。

// input file对象的回调函数
function fileChange({ files }) {
      for(let i = 0; i < files.length; i++) {
      const imgURL =  URL.createObjectURL(files[i]);
      const img = document.createElement('img');
      img.src= imgURL;
      document.body.append(img);
      img.onload = function() {
        URL.revokeObjectURL(imgURL);
        img = null;
      }
    }
}

11-2:Data URL

前缀为data:协议的URL,主要作用就是在HTML文档中嵌入小文件。

显示格式

data:[<mediatype>][;base64],<data>
  
// 纯文本Hello, Word!
data:,Hello%2C%20World!
// html 
data:text/html,<html><script>alert('hi');</script><body>1111</body></html>

主要分为4个部分:

  • 协议头 data: 
  • mediatype :说明MIME类型+编码方式的字符串。默认值是 text/plain;charset=US-ASCII 。
  • base64  编码设定选项(可选项)。非文本类型的数据可以通过base64编码之后再嵌入。通过base64编码后,对于一些二进制的文件,比如图片,会使数据内容变得更加简短。
  • 这个Data URL承载的内容数据。

通过FileReader对象的readAsDataURL()方法,来读取Blob或者File对象的数据,并转化为Date URL字符串来表示读取的内容。

function fileChange({ files }) { 
	for(let i = 0; i < files.length; i++) {
    const render = new FileReader();
    render.readAsDataURL(files[i]);
    render.onload = function() {
      const img = document.createElement('img');
      img.src = render.result;
      document.body.append(img);
      img = null;
    }
  }
}

...持续更新

有问题希望积极指出哦~