手撕 Javascript 核心知识点, 从 0 到 1 实现原生方法 !

我心飞翔 分类:javascript

大家好, 我是耗子

大哥大姐来了先别走呀, 放心, 本文都是满满的干货! 快来测测你都掌握了这些知识点了吗?

别走呀别走呀, 即使会了, 也不妨扫一眼, 万一里面有亿些小细节值得学习呢? (Ow O)/~

image.png

前言

感觉连着几篇文章都是可怜的访问量, 这就是萌新无人看, 菜的抠jio嘛 /(ㄒoㄒ)/~~ 求一波支持!

这篇文章总结了重要且常见的 Javascript 知识点, 并针对每个知识点讲解:

  • 基本介绍
  • 原理与实现步骤
  • 手写实现

希望能够通过分析知识点及实现, 然后加亿点细节, 升华对其的理解, 以此讨个赞 👍👍👍 疯狂暗示!

如果文章有所不足, 请您务必务必告诉我, 我一定改 /(ㄒoㄒ)/~~

大家可以参照目录选取自己需要的部分进行阅读, 内容将不定时更新, 建议收藏! (给赏个脸233)

1. 手写 new 方法

原理

首先呢, new 方法主要实现的功能如下:

  • 创建一个空对象 ( prototype 源于 Object.prototype )
  • 区分 constructor 和 其他参数
  • 设置对象原型
  • 在对象上调用构造函数
  • 如果构造函数返回对象, 则返回此对象, 否则返回 新建对象

上述功能中涉及原型链的部分, 我们可以使用 __proto__ 属性, 不过官方推荐的是使用 Object.getPrototypeOfObject.setPrototypeOf

每个函数都具有一个原型对象 prototype, constructor 是原型对象指向构造函数的属性。

实现

let newObject = function() {
    let obj = new Object(),
        Constructor = Array.prototype.shift.call(argument);
    // obj.__proto__ = Constructor.prototype; //废弃
    Object.setPrototypeOf(obj, Constructor.prototype);
    
    let ret = Constructor.apply(obj, arguments);
    return typeof ret === 'object' ? ret : obj;
}
 

如果你熟练 es6 , 那么上述代码其实可以用剩余参数来优化的。

  • 提取 constructor 和 其他参数

    function (constructor, ...arguments){}
     
  • 合并新建对象操作

    Object.create 可以替代文中的三行操作, 具体实现可看下一点

    Object.create(constructor.prototype);
    let ret = constructor.apply(obj, arguments);
    
    // 等价于
    
    let obj = new Object(),
        Constructor = Array.prototype.shift.call(argument);
    // obj.__proto__ = Constructor.prototype; //废弃
    Object.setPrototypeOf(obj, Constructor.prototype);
    let ret = Constructor.apply(obj, arguments);
     

2. 手写 Object.create 方法

上一个实例, 我们用到了一个 Object.create, 现在我们就来聊聊它。

主要功能是将现有对象作为原型对象生成一个新的对象, 这个新对象的 __proto__ 属性指向原型对象。

如果将 null 作为参数, 因为 null 是原型链的终点, 所以它将返回一个全空对象(没有原型链)。

原理

  1. 首先我们需要一个孵化新对象的容器, let F = function(){}
  2. 然后我们配置容器, 生成我们要的对象, F.prototype = argsObject
  3. 生成我们需要的对象, return new F()

实现

function ObjectCreate(proto) {
    let F = function(){};
    F.prototype = obj;
    return new F();
}
 

3. 手写 类的继承(原型链与寄生组合继承)

object.create 都说了, 不说类的继承过的去?

原型链和继承是每一个Web前端开发者都逃不掉坎, 现在我们就利用刚刚说的 Object.create 方法实现继承。

原理

javascript 中你有多种方式实现继承, 但是资源利用最优的依旧是寄生组合继承。

javascript 的继承是通过原型链实现的, 每个对象(包括函数)都有一个原型对象, 子类能够读取到父类及祖类的原型对象, 从而访问父类和祖类的方法, 达到继承的目的。

具体的参见红宝书, 这里就不细说啦~

步骤如下:

  1. 通过父类构造函数构建 子类实例属性: 子类借调父类构造函数, 在实例上创建父类实例属性, 并添加新的子类属性。
  2. 继承父类属性: 根据父类原型创建原型对象 (防止创建父类实例, 造成父类实例属性冗余), 绑定子类构造函数和原型。
  3. 增强新创建的原型对象: 添加子类原型属性和方法:。

这也就是es6 中的 class, extends 相关关键字原理。

实现

function Super(name) {
    this.name = name;
    this.colors = ["red", "blue"];
}
Super.prototype.sayName = function() { console.log(this.name); };

function Sub(name, age) {
    Super.call(this, name);
    this.age = age;
}

(function inheritPrototype(Sub, Sup) {
    // 根据父类原型对象创建子类原型对象, 避免创建父类实例和冗余属性
    let prototype = Object.create(Sup.prototype);
    // 绑定新 原型与子类构造函数的关系
    prototype.constructor = Sub;
    Sub.prototype = prototype;
})(Sub, Super);

// 绑定原型后, 在新原型对象上绑定新方法
Sub.prototype.sayAge = function() { console.log(this.age); }
 

4. 手写 箭头函数

原理

箭头函数是 es6 重要的新特性之一, 箭头函数自身没有 arguments, prototype, caller 等参数, 甚至没有自己的 this, 而是绑定词法执行域最近的 this

因此 箭头函数也不能作为构造函数使用, 不能与 new 一起使用 (this都没有, 也配生成对象?)。

不过箭头函数仍然拥有函数对象的原型链, 但是在调用 callapply 的时候无法绑定 this, 会被忽略。

箭头函数本质上其实就是我们早期 es5 绑定作用域的一种增强:

function () {
    let that = this;
    setTimeout(function() {
        return that;
    }, 1000);
}
 

Babel 转义 es5 实现

箭头函数的实现其实就是我们常用的 传递 this 指针的方式, 创建一个闭包保存引用。

function A() {
    return ()=>{console.log(this)}
}

funcion funA() {
    return (function(){console.log(this);}).bind(this);
}
 

image.png

5. 手写 call 与 apply 函数

承接上一个知识点, 我们现在来实现 Function.prototype.callFunction.prototype.apply

在es6中, 这两个方法被收入 Reflext , 分别为 Reflect.callReflect.apply , 原理相同。

callapply 都是被用作借调(改变this指向, 并立即调用):

Array.prototype.join.call(["1", "2"], " ")
 

上述代表表示, 在 this 指向 (也就是数组 ["1", "2"]) 的情况下 调用 .split(" ")

func.apply(thisArg, [argsArray])
 

参数1: 函数接受一个 this , 绑定函数执行域;

callapply 最大的区别在于, call 接受参数列表, 而 apply 接受参数数组

原理

首先, callapply 都是函数原型上的方法, 我们需要在 Function.prototype 中定义

其次两者差别就是提取参数的不同, 这里就以 call 实现为主。

  1. 判断是否是函数, 不是函数不可被借调
  2. 分离参数中的参数
  3. 确定借调目标
  4. 在 借调目标 创建此方法 (链接作用域)
  5. 执行, 并保存执行结果
  6. 删除创建的方法
  7. 返回结果

实现

Function.prototype.call = function (thisArg) { 
    
    // 先判断当前的甲方是不是一个函数(this就是Product,判断Product是不是一个函数)
    if (typeof this !== 'function') {
        throw new TypeError('当前调用call方法的不是函数!');
    }
    
    // 提取参数列表, 如果是 apply 就获取 argument[1]
    const args = [...arguments].slice(1);
    
    // 如果 thisArg 为 null 或 undefined
    thisArg = thisArg || window;
    
    // 创建唯一值
    const fn = Symbol('fn');
    
    // 将此方法添加到目标上
    thisArg[fn] = this;
    
    // 执行保存的函数,这个时候作用域就是在目标对象的作用域下执行
    const result = thisArg[fn](...args);
    
    // 执行完删除刚才新增的属性值
    delete thisArg[fn];
    
    // 返回执行结果
    return result;
}
 

如果是 apply 就是用 const args = [...arguments].slice(1);

6. 手写 bind 函数

bind 就是在 applycall 的基础上, 我不希望它直接被调用, 而是在我需要的时候调用。

image.png

原理

绑定 this 指向并返回一个函数, 函数执行时调用 call / apply

  1. 保存原函数引用, 也就是 this
  2. 防止报错 和 提取参数
  3. 返回一个函数
  4. 返回的函数中 二次取参
  5. new 操作的优先级大于bind , 针对 new 调用额外判断

实现

1. 基本实现思路

Function.prototype.bind1 = function( context, ...args ){
    var self = this;
    return function(){
        return self.apply( context, args); 
    }
};
 

2. 添加报错和参数提取

参数提取有两次: bind时传入的参数, 和调用返回函数的时候传入的参数

Function.prototype.bind2 = function (context) {
    if (typeof this !== 'function') {
        throw new Error('Function.prototype.bind - what is trying to be bound is not callable')
    }
    let f = this;
    let agrs = [...arguments].slice(1) //第一次取参数
    let bindFun =  function () {
        let agrs2 = [...arguments]  //第二次取参
        let Allagrs = agrs.concat(agrs2)    //合并两组参数(要注意一下顺序)
        f.apply(context, Allagrs)  //使用apply修改this的指向s
    }
    return bindFun
}
 

3. 针对 new 的调用判断

如果时函数调用, 则this指向新创建的对象

Function.prototype.bind3 = function (context) {
    if (typeof this !== 'function') {
        throw new Error('Function.prototype.bind - what is trying to be bound is not callable');
    };
    
    let f = this;
    
    let agrs = [...arguments].slice(1); //第一次取参数(调用bind2时传入的参数)
    
    let bindFun =  function () {
        let agrs2 = [...arguments];
        let Allagrs = agrs.concat(agrs2);
        
        // 如果时函数调用, 则this指向新创建的对象
        f.apply(this instanceof f ? this : context, Allagrs);
    }
    bindFun.prototype = f.prototype //原型链继承
    return bindFun
}
 

7. 手写更加全面的 typeof

typeofjavascript 中常用的类型判断操作符, 不过它却有所缺陷。

我们可以利用 typeof 来判断 number, string , object , boolean , function , undefined , symbol , bigint 这八种类型。

这是基于 javascript 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息, 大致如下:

  • 000:对象
  • 010:浮点数
  • 100:字符串
  • 110: 布尔值
  • 1:整数
  • -2^30: undefined

其中所有类似对象(Array等) 以及 null (全0, 末尾000标识为对象) 返回 object

鉴于如此, 我们一般使用 toString 来判断对象。

原理

基于 Object.prototype.toString 的借调实现:

image.png

根据 MDN 给出的 type 实现, 判断步骤如下:

  1. 判断是否为 null 或者 undefined

  2. 防止过度解读, 类似 HTMLDivElement 的元素, 先获取后使用正则过滤

    deepType.match(/^(array|bigint|date|error|function|generator|regexp|symbol)$/)

  3. 普通类型直接使用 typeof 返回

实现

MDN 给出的实现中有两处不解:

  • 这里的 fullClass 个人感觉有点多余
  • 最后为什么还要判断一次 typeof obj === 'function', 之前的 match 应该匹配到了

可能学识尚浅, 如果有大佬看到了, 希望可以给个解答。

MDN 版本

function type(obj, fullClass) {
    if (fullClass) {
        return (obj === null) ? '[object Null]' : Object.prototype.toString.call(obj);
    }
    if (obj == null) { return (obj + '').toLowerCase(); }

    var deepType = Object.prototype.toString.call(obj).slice(8,-1).toLowerCase();
    if (deepType === 'generatorfunction') { return 'function' }

    return deepType.match(/^(array|bigint|date|error|function|generator|regexp|symbol)$/) ? deepType :
       (typeof obj === 'object' || typeof obj === 'function') ? 'object' : typeof obj;
}
 

8. 手写 instanceof

这是一个用来判断 实例是否属于某类型 的方法。

过于常用我也就不多说废话了, 直接上原理。

原理

判断依据:

  1. Symbol.hasInstance 可以重写 instanceof 行为 (默认参照原型链), 定义在构造函数上:

    class MyArray {
      static [Symbol.hasInstance](instance) {
        return Array.isArray(instance);
      }
    }
    console.log([] instanceof MyArray); // true
     

    甚至我们可以离谱一点:

    image.png

  2. 不同窗口的数组实现可能不同, 可以使用 Array.isArray 判断

  3. 原型链

  • 通过实例上的 __proto__ 或使用 Object.getPrototypeOf 获取原型对象
  • 通过原型对象比较类型, 或原型对象的 constructor 比较类型
  • 如果不同则继续查询原型链, 即 __proto__.__proto__, 直到原型链终点 null

实现

function myInstanceof(instance, object) {
    if (object[Symbol.hasInstance]) return object[Symbol.hasInstance](instance);
    else {
        if (instance === null || instance !== 'object') return false;
        if (object === Array) return Array.isArray(instance);

        let proto = Object.getPrototypeOf(instance);
        while(proto !== null) {
            if(proto == object.prototype) return true;
            proto = Object.getPrototypeOf(proto);
        }
        return false;
    }
}
 

9. 手写 深拷贝

所谓的浅拷贝, 指的是 引用 的拷贝, 而所谓的深拷贝指的是 的拷贝:

  • 浅拷贝
let obj = {a:1};
let obj2 = obj;
obj2.a = 2;
console.log(obj); // {a:2}
 

实际上 objobj2 指向同一个内存区域的标识, 所以修改也是一致。

Object.assign(A, B) 就是将B的所有引用值, 添加到A;

  • 深拷贝

而深拷贝要做的就是, 拷贝其中的值, 断开引用重复。

let obj = {a:1};
let obj2 = {};
obj2.a = obj.a;
obj2.a = 2;
console.log(obj); // {a:1}
 

原理

有两种方法, 一种是将转化为字符串, 再根据字符串重新构建数组;

另一种方法是, 循环递归, 拷贝对象结构。

而循环递归, 我们需要防止:

  1. 循环引用
  2. 同一引用对象多次拷贝

步骤如下:

  • 通过数组记录遍历过的对象 (拷贝前, 拷贝后)
  • 判断是否是对象, 如果是对象进行深拷贝

实现

  1. JSON.parse 和 JSON.stringify
  • 值为 function、正则、Symbol 的属性不会被拷贝
  • 有可能导致循环引用
  • 相同引用被多次拷贝, 即 a.a = a.b = {} , 但是拷贝结果 a.aa.b 都为 {} 却不相等
  1. 循环递归拷贝
function deepClone(obj) {
    let copyed = new Map();
    
    function _deepClone (obj) {
        if (obj && typeof obj === "object") {
            
            let objClone = Array.isArray(obj) ? [] : {};
            if (!copyed.has(obj)) copyed.set(obj, objClone); 
            for ( key in obj ) {
                if( obj.hasOwnProperty(key) ) {
                    if ( obj[key] && typeof obj[key] === "object" ) {    
                        if (copyed.has(obj[key])) objClone[key] = copyed.get(obj[key]);
                        else objClone[key] = _deepClone(obj[key]);
                    }else{
                        objClone[key] = obj[key];
                    }
                }
            }
            return objClone;
        } else return obj;
    }
    return _deepClone(obj);
}
 

image.png

image.png

10. 手写 数组去重

原理

基于 Object.prototype.toString.call 实现。

我们需要判断:

  • symbol (都不重复), 但是不可参与运算, 所以 ""=Symbol(1) 的隐式转化会报错哦

    要判重同值 Symbol 就调用 Symbol(1).toString()

  • 对象

    内容不判重, 判断是否有过相同引用

    内容判重仅对象可用 JSON.stringify

  • NaN 单独判断且唯一, 通过 Number.isNaNhasNaN 标记

  • 其他数值

    包括 null 与 undefined

    使用 temp 存储 typeof + String(obj) 保存为键值, 判重

实现

Array.prototype.uniq = function () {
    if (!this.length || this.length == 0) return this;
    var res = [], key, hasNaN = false, temp = {};
    for (var i = 0 ; i < this.length; i++) {
        if (typeof this[i] === 'symbol') {
            res.push(this[i]);
        } else if (Object.prototype.toString.call(this[i]) === '[object Object]') {
            key = typeof(this[i]) + JSON.stringify(this[i]);
            if (!temp[key]) {
                res.push(this[i]);
                temp[key] = true;
            }            
        } else if (Number.isNaN(this[i])) { // 如果当前遍历元素是NaN
            if (!hasNaN) {
                res.push(NaN);
                hasNaN = true;
            }
        } else {
            key = typeof(this[i]) + this[i];
            if (!temp[key]) {
                res.push(this[i]);
                temp[key] = true;
            }
        }
    }
    return res;
}
 

image.png

11. 手写 数组扁平化 flat

所谓数组扁平化就是将嵌套数组展开:

image.png

原理

我们需要传入一个参数 n 来控制展开层数,

这里我才用的方法是递归, 当然相信你们一定有更好的~

  1. 使用 n 控制递归层数, 原生 flat 默认为 1
  2. 判断是否到达最大递归层, 如果是, 直接返回元素
  3. 判断当前项是数组还是其他数据
  4. 递归使用 reduceconcat 拼接并返回结果

实现

function flatArray(arr, n = 1) {
    function deep(item, i) {
        if (i === n) {
            return item;
        }
        else {
            if (Array.isArray(item)) {
                return item.reduce((prev, cur)=> {
                   return prev.concat(deep(cur, i+1)); 
                }, []);
            } else return item;
        }
    }
    return deep(arr, 0);
}
 

11. 手写防抖函数 debounce

所谓的防抖函数, 正如其名, 防止抖腿抖动!

主要用于防止频繁触发事件, 使其在不论触发多少次, 只有间隔最后一次一定时间后, 才执行任务。

常用于 onmousemove , onresize, 搜索输入框 等高频触发事件:

image.png

原理

  1. 设定一个定时器
  2. 第一次触发时间, 激活一个定时器
  3. 定时器有效期间触发事件都会重置计时器 (清楚原来的, 重新创建一个)
  4. 最后一次触发并间隔一定时间, 定时器结束, 触发事件;
  5. 清空 timer 。

可以相到, 这应该需要使用到闭包来存储定时器。

实现

function debounce(fn, delay) {
    let timer;
    return function (...args) {
        var _this = this; 保存创建函数的作用域
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(function () {
            fn.apply(_this, args);
            clearTimeout(timer);
        }, delay);
    };
}
 

12. 手写 节流函数 throttle

节流函数其实与防抖函数有些类似, 不过又有所不同。

节流函数有点像游戏角色的技能, 一定时间只能使用一次。

换个简单的说法, 冷却CD

正如其名, 节流常用来控制网络请求的发送频率, 以此控制流量, 比如登陆验证, 搜索等。

原理

有两种实现方式, 本质都是基于 时间。

  1. 定时器⏲ 计时
  2. 时间戳 计时

同样, 也需要闭包来判断是否冷却完毕, 可以执行。

每次执行事件过后, 设置一个定时器, 定时期间, 无法执行时间。

实现

  1. 定时器
function throttle(func, delay = 100) {
    let canRun = true;
    return function (...args) {
        let _this = this;
        if (!canRun) {
            return;
        }
        func.apply(_this, args);
        canRun = false;
        setTimeout(function () {
            canRun = true;
        }, delay)
    }
}
 
  1. 时间戳
function throttle(func, delay = 100) {
    let previous;
    return function (...args) {
        let _this = this;
        let now = +new Date()
        if (now - previous <= delay ) {
            return;
        }
        func.apply(_this, args);
        previous = +new Date();
    }
}
 

13. 手写 sleep 函数

什么是 sleep 函数? 简单说就是 __ 咋瓦鲁多

image.png

也就是时停!

我们需要让我们的任务, 间隔一定时间执行, 或者停止 n 秒后执行。

首先明确一点, JavaScript 虽然是单线程, 但是 setTimeout 是基于浏览器模块实现的, 它会开启一个计时器, 每个计时器互不影响。

image.png

所以上述代码最终会一起弹出。

兼容的一种方案是, 使用递增计时器:

setTimeout(func(), 100);setTimeout(func(), 200);

或者回调函数调用:

setTimeout(()=>{func();setTimeout(func(), 100);}, 100);

但是这实在不够优雅,

这时候还是来整点阳间的活, 这就要请到我们的异步调用了 async/await (生成器和promise的语法糖)

原理

我们需要实现一个 sleep 函数用来即时, 并且返回一个promise

而 外部有一个 async 执行函数, 通过 await 等待 sleep

以此达到时停的目的。

实现

function sleep(delay) {
    return new Promise((resolve)=>{
        setTimeout(()=>resolve(), delay);
    });
}
 

image.png

当然, 需要注意的是, sleep 并不准时, 它有可能延期执行, 具体可以查看 事件循环队列。

结语

非常感谢您的观看, 您的点赞是对我最好的支持!

保持学习! 我还会回来的~ 你有什么想法欢迎评论区告诉我~

我会在未来不定时的更新这篇文章, 如果你觉得不错记得收藏, 有事没事, 温故而知新~

参考文章

  • MDN
  • 浅谈Object.prototype.toString.call()方法
  • JS深拷贝总结
  • 彻底弄懂函数防抖和函数节流

回复

我来回复
  • 暂无回复内容