【高频JS手写】20+高频JS手写题总结

9,268 阅读19分钟

前言

这篇文章主要是 总结 + 实践 一些比较常见且重要的JS手写题,方便自己以及大家学习参考。

1. 数据类型判断

核心思想typeof 可以判断 Undefined、String、Number、Boolean、Symbol、Function类型的数据,但对其他的都会认为是Object,比如Null、Array等。所以通过typeof来判断数据类型会不准确。

解决方法:可以通过Object.prototype.toString解决。

实现

function mytypeof(obj) {
    return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
  1. 使用call 是为了绑定 thisobj
  2. 使用slice 是因为这前面返回的结果是类似[Object xxx]这样的, xxx 是根据 obj 的类型变化的
  3. 使用toLowerCase 是因为原生typeof的返回结果的第一个字母是小写字母。

2. 继承

2.1 原型继承

核心思想:子类的原型成为父类的实例

实现

function SuperType() {
    this.colors = ['red', 'green'];
}
function SubType() {}
// 原型继承关键: 子类的原型成为父类的实例
SubType.prototype = new SuperType();

// 测试
let instance1 = new SubType();
instance1.colors.push('blue');

let instance2 = new SubType();
console.log(instance2.colors);  // ['red', 'green', 'blue']

原型继承存在的问题

  1. 原型中包含的引用类型属性将被所有实例对象共享
  2. 子类在实例化时不能给父类构造函数传参

2.2 构造函数继承

核心思想:在子类构造函数中调用父类构造函数

实现

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'green'];
    this.getName = function() {
        return this.name;
    }
}
function SubType(name) {
    // 继承 SuperType 并传参
    SuperType.call(this, name);
}

// 测试
let instance1 = new SubType('instance1');
instance1.colors.push('blue');
console.log(instance1.colors); // ['red','green','blue']

let instance2 = new SubType('instance2');
console.log(instance2.colors);  // ['red', 'green']

构造函数继承的出现是为了解决了原型继承的引用值共享问题。优点是可以在子类构造函数中向父类构造函数传参。它存在的问题是:1)由于方法必须在构造函数中定义,因此方法不能重用。2)子类也不能访问父类原型上定义的方法。

2.3 组合继承

核心思想:综合了原型链和构造函数,即,使用原型链继承原型上的方法,而通过构造函数继承实例属性。

实现

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'green'];
}
Super.prototype.sayName = function() {
    console.log(this.name);
}
function SubType(name, age) {
    // 继承属性
    SuperType.call(this, name);
    // 实例属性
    this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();

// 测试
let instance1 = new SubType('instance1', 1);
instance1.sayName();  // "instance1"
instance1.colors.push("blue");
console.log(instance1.colors); // ['red','green','blue']

let instance2 = new SubType('instance2', 2);
instance2.sayName();  // "instance2"
console.log(instance2.colors); // ['red','green']

组合继承存在的问题是:父类构造函数始终会被调用两次:一次是在创建子类原型时new SuperType()调用,另一次是在子类构造函数中SuperType.call()调用。

2.4 寄生式组合继承(最佳)

核心思想:通过构造函数继承属性,但使用混合式原型继承方法,即,不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。

实现

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'green'];
}
Super.prototype.sayName = function() {
    console.log(this.name);
}
function SubType(name, age) {
    // 继承属性
    SuperType.call(this, name);
    this.age = age;
}
// 继承方法
SubType.prototype = Object.create(SuperType.prototype);
// 重写原型导致默认 constructor 丢失,手动将 constructor 指回 SubType
SubType.prototype.constructor = SubType;

2.5 class 实现继承(ES6)

核心思想:通过 extends 来实现类的继承(相当于 ES5 的原型继承)。通过 super 调用父类的构造方法 (相当于 ES5 的构造函数继承)。

实现

class SuperType {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
}
class SubType extends SuperType {
    constructor(name, age) {
        super(name);  // 继承属性
        this.age = age;
    }
}

// 测试
let instance = new SubType('instance', 0);
instance.sayName();  // "instance"

虽然类继承使用的是新语法,但背后依旧使用的是原型链。

3. 数组去重

3.1 使用 indexOf/includes 实现

function unique(arr) {
    var res = [];
    for(var i = 0; i < arr.length; i++) {
        if(res.indexOf(arr[i]) === -1) res.push(arr[i]);
        // if(!res.includes(arr[i])) res.push(arr[i]);
    }
    return res;
}

3.2 使用 filter(forEach) + indexOf/includes 实现

// filter
function unique(arr) {
    var res = arr.filter((value, index) => {
        // 只存第一个出现的元素
        return arr.indexOf(value) === index;
    });
    return res;
}
// forEach
function unique(arr) {
    var res = [];
    arr.forEach((value) => {
        if(!res.includes(value)) res.push(value);
    });
    return res;
}

3.3 非 API 版本(原生)实现

function unique(arr) {
    var res = [];
    for(var i = 0; i < arr.length; i++) {
        var flag = false;
        for(var j = 0; j < res.length; j++) {
            if(arr[i] === res[j]) {
                flag = true;
                break;
            }
        }
        if(flag === false) res.push(arr[i]);
    }
    return res;
}

3.4 ES6 使用 Set + 扩展运算符(...)/Array.from() 实现

function unique(arr) {
    // return [...new Set(arr)];
    return Array.from(new Set(arr));
}

4. 数组扁平化

4.1 ES5 递归写法 —— isArray()、concat()

function flat(arr) {
    var res = [];
    for (var i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            res = res.concat(flat(arr[i]));
        } else {
            res.push(arr[i]);
        }
    }
    return res;
}

如果想实现第二个参数(指定“拉平”的层数),可以这样实现,后面的几种可以自己类似实现:

function flat(arr, level = 1) {
    var res = [];
    for(var i = 0; i < arr.length; i++) {
        if(Array.isArray(arr[i]) && level >= 1) {
            res = res.concat(flat(arr[i], level - 1));
        }
        else {
            res.push(arr[i]);
        }
    }
    return res;
}

4.2 ES6 递归写法 — reduce()、concat()、isArray()

function flat(arr) {
    return arr.reduce(
        (pre, cur) => pre.concat(Array.isArray(cur) ? flat(cur) : cur), []
    );
}

4.3 ES6 迭代写法 — 扩展运算符(...)、some()、concat()、isArray()

ES6 的扩展运算符(...) 只能扁平化一层

function flat(arr) {
    return [].concat(...arr);
}

全部扁平化:遍历原数组,若arr中含有数组则使用一次扩展运算符,直至没有为止。

function flat(arr) {
    while(arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}

4.4 toString/join & split

调用数组的 toString()/join() 方法(它会自动扁平化处理),将数组变为字符串然后再用 split 分割还原为数组。由于 split 分割后形成的数组的每一项值为字符串,所以需要用一个map方法遍历数组将其每一项转换为数值型。

function flat(arr){
    return arr.toString().split(',').map(item => Number(item));
    // return arr.join().split(',').map(item => Number(item));
}

4.5 使用正则

JSON.stringify(arr).replace(/[|]/g, '') 会先将数组arr序列化为字符串,然后使用 replace() 方法将字符串中所有的[] 替换成空字符,从而达到扁平化处理,此时的结果为 arr 不包含 [] 的字符串。最后通过JSON.parse() 解析字符串。

function flat(arr) {
    return JSON.parse("[" + JSON.stringify(arr).replace(/\[|\]/g,'') + "]");
}

5. 类数组转化为数组

类数组是具有 length 属性,但不具有数组原型上的方法。常见的类数组有 arguments、DOM 操作方法返回的结果(如document.querySelectorAll('div'))等。

5.1 扩展运算符(...)

注意:扩展运算符只能作用于 iterable 对象,即拥有 Symbol(Symbol.iterator) 属性值。

let arr = [...arrayLike]

5.2 Array.from()

let arr = Array.from(arrayLike);

5.3 Array.prototype.slice.call()

let arr = Array.prototype.slice.call(arrayLike);

5.4 Array.apply()

let arr = Array.apply(null, arrayLike);

5.5 concat + apply

let arr = Array.prototype.concat.apply([], arrayLike);

6. 实现数组原型方法

6.1 forEach

语法:arr.forEach(callback(currentValue [, index [, array]])[, thisArg])

参数:

callback:为数组中每个元素执行的函数,该函数接受1-3个参数

currentValue: 数组中正在处理的当前元素

index(可选): 数组中正在处理的当前元素的索引

array(可选): forEach() 方法正在操作的数组 thisArg(可选): 当执行回调函数 callback 时,用作 this 的值。

返回值:undefined

Array.prototype.forEach1 = function(callback, thisArg) {
    if(this == null) {
        throw new TypeError('this is null or not defined');
    }
    if(typeof callback !== "function") {
        throw new TypeError(callback + 'is not a function');
    }
    // 创建一个新的 Object 对象。该对象将会包裹(wrapper)传入的参数 this(当前数组)。
    const O = Object(this);
    // O.length >>> 0 无符号右移 0 位
    // 意义:为了保证转换后的值为正整数。
    // 其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型
    const len = O.length >>> 0;
    let k = 0;
    while(k < len) {
        if(k in O) {
            callback.call(thisArg, O[k], k, O);
        }
        k++;
    }
}

6.2 map

语法: arr.map(callback(currentValue [, index [, array]])[, thisArg])

参数:与 forEach() 方法一样

返回值:一个由原数组每个元素执行回调函数的结果组成的新数组。

Array.prototype.map1 = function(callback, thisArg) {
    if(this == null) {
        throw new TypeError('this is null or not defined');
    }
    if(typeof callback !== "function") {
        throw new TypeError(callback + 'is not a function');
    }
    const O = Object(this); 
    const len = O.length >>> 0;
    let newArr = [];  // 返回的新数组
    let k = 0;
    while(k < len) {
        if(k in O) {
            newArr[k] = callback.call(thisArg, O[k], k, O);
        }
        k++;
    }
    return newArr;
}

6.3 filter

语法:arr.filter(callback(element [, index [, array]])[, thisArg])

参数:

callback: 用来测试数组的每个元素的函数。返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数:element、index、array,参数的意义与 forEach 一样。

thisArg(可选): 执行 callback 时,用于 this 的值。

返回值:一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。

Array.prototype.filter1 = function(callback, thisArg) {
    if(this == null) {
        throw new TypeError('this is null or not defined');
    }
    if(typeof callback !== "function") {
        throw new TypeError(callback + 'is not a function');
    }
    const O = Object(this); 
    const len = O.length >>> 0;
    let newArr = [];  // 返回的新数组
    let k = 0;
    while(k < len) {
        if(k in O) {
            if(callback.call(thisArg, O[k], k, O)) {
                newArr.push(O[k]);
            }
        }
        k++;
    }
    return newArr;
}

6.4 some

语法:arr.some(callback(element [, index [, array]])[, thisArg])

参数:

callback: 用来测试数组的每个元素的函数。接受以下三个参数:element、index、array,参数的意义与 forEach 一样。

thisArg(可选): 执行 callback 时,用于 this 的值。 返回值:数组中有至少一个元素通过回调函数的测试就会返回 true;所有元素都没有通过回调函数的测试返回值才会为 false

Array.prototype.some1 = function(callback, thisArg) {
    if(this == null) {
        throw new TypeError('this is null or not defined');
    }
    if(typeof callback !== "function") {
        throw new TypeError(callback + 'is not a function');
    }
    const O = Object(this); 
    const len = O.length >>> 0;
    let k = 0;
    while(k < len) {
        if(k in O) {
            if(callback.call(thisArg, O[k], k, O)) {
                return true
            }
        }
        k++;
    }
    return false;
}

6.5 reduce

语法:arr.reduce(callback(preVal, curVal[, curIndex [, array]])[, initialValue])

参数:

callback: 一个 “reducer” 函数,包含四个参数:

preVal:上一次调用 callback 时的返回值。在第一次调用时,若指定了初始值 initialValue,其值则为 initialValue,否则为数组索引为 0 的元素 array[0]

curVal:数组中正在处理的元素。在第一次调用时,若指定了初始值 initialValue,其值则为数组索引为 0 的元素 array[0],否则为 array[1]

curIndex(可选):数组中正在处理的元素的索引。若指定了初始值 initialValue,则起始索引号为 0,否则从索引 1 起始。

array(可选):用于遍历的数组。 initialValue(可选): 作为第一次调用 callback 函数时参数 preVal 的值。若指定了初始值 initialValue,则 curVal 则将使用数组第一个元素;否则 preVal 将使用数组第一个元素,而 curVal 将使用数组第二个元素。 返回值:使用 “reducer” 回调函数遍历整个数组后的结果。

Array.prototype.reduce1 = function(callback, initialValue) {
    if(this == null) {
        throw new TypeError('this is null or not defined');
    }
    if(typeof callback !== "function") {
        throw new TypeError(callback + 'is not a function');
    }
    const O = Object(this);
    const len = O.length >>> 0;
    let k = 0;
    let accumulator = initialValue;
    // 如果第二个参数为undefined的情况下,则数组的第一个有效值(非empty)作为累加器的初始值
    if(accumulator === undefined) {
        while(k < len && !(k in O)) {
            k++;
        }
        // 如果超出数组界限还没有找到累加器的初始值,则TypeError
        if(k >= len) {
            throw new TypeError('Reduce of empty array with no initial value');
        }
        accumulator = O[k++];
    }
    while(k < len) {
        if(k in O) {
            accumulator = callback(accumulator, O[k], k, O);
        }
        k++;
    }
    return accumulator;
}

7. call/apply/bind 的实现

7.1 call

描述:使用 一个指定的 this 值(默认为 window)一个或多个参数 来调用一个函数。

语法function.call(thisArg, arg1, arg2, ...)

核心思想

  • 调用call 的可能不是函数
  • this 可能传入 null
  • 传入不固定个数的参数
  • 给对象绑定函数并调用
  • 删除绑定的函数
  • 函数可能有返回值

实现

Function.prototype.call1 = function(context, ...args) {
    if(typeof this !== "function") {
        throw new TypeError("this is not a function");
    }
    context = context || window; // 如果传入的是null, 则指向window
    let fn = Symbol('fn');  // 创造唯一的key值,作为构造的context内部方法名
    context[fn] = this;  // 为 context 绑定原函数(this)
    let res = context[fn](...args); // 调用原函数并传参, 保存返回值用于call返回
    delete context[fn];  // 删除对象中的函数, 不能修改对象
    return res;
}

7.2 apply

描述:与 call 类似,唯一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个参数数组或类数组。

实现

Function.prototype.apply1 = function(context, arr) {
    if(typeof this !== "function") {
        throw new TypeError("this is not a function");
    }
    context = context || window; // 如果传入的是null, 则指向window
    let fn = Symbol('fn');  // 创造唯一的key值,作为构造的context内部方法名
    context[fn] = this;  // 为 context 绑定原函数(this)
    let res;
    // 判断是否传入的数组是否为空
    if(!arr) {
        res = context[fn]();
    }
    else {
        res = context[fn](...arr); // 调用原函数并传参, 保存返回值用于call返回
    }
    delete context[fn];  // 删除对象中的函数, 不能修改对象
    return res;
}

7.3 bind

描述bind 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

核心思想:

  • 调用bind的可能不是函数
  • bind() 除了 this 外,还可传入多个参数
  • bind() 创建的新函数可能传入多个参数
  • 新函数可能被当做构造函数调用
  • 函数可能有返回值

实现

Function.prototype.bind1 = function(context, ...args) {
    if (typeof that !== "function") {
        throw new TypeError("this is not function");
    }
    let that = this;  // 保存原函数(this)
    return function F(...innerArgs) {
        // 判断是否是 new 构造函数
        // 由于这里是调用的 call 方法,因此不需要判断 context 是否为空
        return that.call(this instanceof F ? this : context, ...args, ...innerArgs);
    }
}

8. new 实现

描述new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。

核心思想:

  • new 会产生一个新对象
  • 新对象需要能够访问到构造函数的属性,所以需要重新指定它的原型
  • 构造函数可能会显示返回对象与基本类型的情况(以及null)

步骤:使用new命令时,它后面的函数依次执行下面的步骤:

  1. 创建一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的隐式原型(__proto__),指向构造函数的prototype属性。
  3. 让函数内部的this关键字指向这个对象。开始执行构造函数内部的代码(为这个新对象添加属性)。
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

实现

// 写法一:
function myNew() {
    // 将 arguments 对象转为数组
    let args = [].slice.call(arguments);
    // 取出构造函数
    let constructor = args.shift();
    
    // 创建一个空对象,继承构造函数的 prototype 属性
    let obj = {};
    obj.__proto__ = constructor.prototype;
    
    // 执行构造函数并将 this 绑定到新创建的对象上
    let res = constructor.call(obj, ...args);
    // let res = constructor.apply(obj, args);
    
    // 判断构造函数执行返回的结果。如果返回结果是引用类型,就直接返回,否则返回 obj 对象
    return (typeof res === "object" && res !== null) ? res : obj;
}

// 写法二:constructor:构造函数, ...args:构造函数参数
function myNew(constructor, ...args) {
    // 生成一个空对象,继承构造函数的 prototype 属性
    let obj = Object.create(constructor.prototype);
    
    // 执行构造函数并将 this 绑定到新创建的对象上
    let res = constructor.call(obj, ...args);
    // let res = constructor.apply(obj, args);
    
    // 判断构造函数执行返回的结果。如果返回结果是引用类型,就直接返回,否则返回 obj 对象
    return (typeof res === "object" && res !== null) ? res : obj;
}

9. 函数柯里化

柯里化(currying) 指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数。

对于已经柯里化后的函数来说,当接收的参数数量与原函数的形参数量相同时,执行原函数; 当接收的参数数量小于原函数的形参数量时,返回一个函数用于接收剩余的参数,直至接收的参数数量与形参数量一致,执行原函数。

9.1 原函数形参定长(此时 fn.length 是个不变的常数)

// 写法1-不保存参数,递归局部函数
function curry(fn) {
    let judge = (...args) => {
        // 递归结束条件
        if(args.length === fn.length) return fn(...args);
        return (...arg) => judge(...args, ...arg);
    }
    return judge;
}

// 写法2-保存参数,递归整体函数
function curry(fn) {
    // 保存参数,除去第一个函数参数
    let presentArgs = [].slice.call(arguments, 1);
    // 返回一个新函数
    return function(){
        // 新函数调用时会继续传参
        let allArgs = [...presentArgs, ...arguments];
        // 递归结束条件
        if(allArgs.length === fn.length) {
            // 如果参数够了,就执行原函数
            return fn(,,,allArgs);
        }
        // 否则继续柯里化
        else return curry(fn, ...allArgs);
    }
}

// 测试
function add(a, b, c, d) {
  return a + b + c + d;
}
console.log(add(1, 2, 3, 4));
let addCurry = curry(add);
// 以下结果都返回 10
console.log(addCurry(1)(2)(3)(4));  
console.log(addCurry(1)(2, 3, 4));
console.log(addCurry(1, 2)(3)(4));
console.log(addCurry(1, 2)(3, 4));
console.log(addCurry(1, 2, 3)(4));
console.log(addCurry(1, 2, 3, 4));

9.2 原函数形参不定长(此时 fn.length 为0)

function curry(fn) {
    // 保存参数,除去第一个函数参数
    let args = [].slice.call(arguments, 1);
    // 返回一个新函数
    let curried = function () {
        // 新函数调用时会继续传参
        let allArgs = [...args, ...arguments];
        return curry(fn, ...allArgs);
    };
    // 利用toString隐式转换的特性,当最后执行函数时,会隐式转换
    curried.toString = function () {
        return fn(...args);
    };
    return curried;
}

// 测试
function add(...args) {
  return args.reduce((pre, cur) => pre + cur, 0);
}
console.log(add(1, 2, 3, 4));
let addCurry = curry(add);
console.log(addCurry(1)(2)(3) == 6); // true
console.log(addCurry(1, 2, 3)(4) == 10); // true
console.log(addCurry(2, 6)(1).toString()); // 9
console.log(addCurry(2, 6)(1, 8)); // 打印 curried 函数

10. 防抖和节流

10.1 防抖

防抖(debounce):触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。类似王者荣耀的回城功能,你反复触发回城功能,那么只认最后一次,从最后一次触发开始计时。

核心思想:每次事件触发就清除原来的定时器,建立新的定时器。使用apply或call调用传入的函数。函数内部支持使用 this 和 event 对象;

应用:防抖常应用于用户进行搜索输入节约请求资源,window触发resize事件时进行防抖只触发一次。

实现

function debounce(fn, delay) {
    // 利用闭包的原理
    let timer = null;
    return function(...args){
        if(timer) clearTimeout(timer);
        timer = setTimeout(() => {
            // 改变 this 指向为调用 debounce 所指的对象
            fn.call(this, ...args);
            // fn.apply(this, args);
        }, delay);
    }
}

10.2 节流

节流(throttle):触发高频事件,且 N 秒内只执行一次。这就好比公交车,10 分钟一趟,10 分钟内有多少人在公交站等我不管,10 分钟一到我就要发车走人!类似qq飞车的复位按钮。

核心思想:使用时间戳或标志来实现,立即执行一次,然后每 N 秒执行一次。如果N秒内触发则直接返回。

应用:节流常应用于鼠标不断点击触发、监听滚动事件。

实现:

// 版本一:标志实现
function throttle(fn, wait){
    let flag = true;  // 设置一个标志
    return function(...args){
        if(!flag) return;
        flag = false;
        setTimeout(() => {
            fn.call(this, ...args);
            flag = true;
        }, wait);
    }
}

// 版本二:时间戳实现
function throttle(fn, wait) {
    let pre = 0;
    return function(...args) {
        let now = new Date();
        if(now - pre < wait) return;
        pre = now;
        fn.call(this, ...args);
    }
}

11. instanceof

作用:判断对象的具体类型。可以区别 arrayobjectnullobject 等。

语法A instanceof B

如何判断的?: 如果B函数的显式原型对象在A对象的原型链上,返回true,否则返回false

注意:如果检测原始值,则始终返回 false

实现:

function myinstanceof(left, right) {
    // 基本数据类型都返回 false,注意 typeof 函数 返回"function"
    if((typeof left !== "object" && typeof left !== "function") || left === null) return false;
    let leftPro = left.__proto__;  // 取左边的(隐式)原型 __proto__
    // left.__proto__ 等价于 Object.getPrototypeOf(left)
    while(true) {
        // 判断是否到原型链顶端
        if(leftPro === null) return false;
        // 判断右边的显式原型 prototype 对象是否在左边的原型链上
        if(leftPro === right.prototype) return true;
        // 原型链查找
        leftPro = leftPro.__proto__;
    }
}

12. 深/浅拷贝

首先判断数据类型是否为对象,如果是对象(数组|对象),则递归(深/浅拷贝),否则直接拷贝。

function isObject(obj) {
    return typeof obj === "object" && obj !== null;
}

这个函数只能判断 obj 是否是对象,无法判断其具体是数组还是对象。

12.1 浅拷贝

// 这里只考虑对象类型
function shallowClone(obj) {
    if(!isObject(obj)) return obj;
    let newObj = Array.isArray(obj) ? [] : {};
    // for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)
    for(let key in obj) {
        // obj.hasOwnProperty() 方法只考虑对象自身的属性
        if(obj.hasOwnProperty(key)) {
            newObj[key] = obj[key];
        }
    }
    return newObj;
}

12.2 深拷贝

实现一:不考虑 Symbol

function deepClone(obj) {
    if(!isObject(obj)) return obj;
    let newObj = Array.isArray(obj) ? [] : {};
    // for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)
    for(let key in obj) {
        // obj.hasOwnProperty() 方法只考虑对象自身的属性
        if(obj.hasOwnProperty(key)) {
            newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key];
        }
    }
    return newObj;
}

实现二:考虑 Symbol

// hash 作为一个检查器,避免对象深拷贝中出现环引用,导致爆栈
function deepClone(obj, hash = new WeakMap()) {
    if(!isObject(obj)) return obj;
    // 检查是有存在相同的对象在之前拷贝过,有则返回之前拷贝后存于hash中的对象
    if(hash.has(obj)) return hash.get(obj);
    let newObj = Array.isArray(obj) ? [] : {};
    // 备份存在hash中,newObj目前是空对象、数组。后面会对属性进行追加,这里存的值是对象的栈
    hash.set(obj, newObj);
    // Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
    Reflect.ownKeys(obj).forEach(key => {
        // 属性值如果是对象,则进行递归深拷贝,否则直接拷贝
        newObj[key] = isObject(obj[key]) ? deepClone(obj[key], hash) : obj[key];
    });
    return newObj;
}

13. Object.assign()

描述Object.assign()方法用于将所有可枚举Object.propertyIsEnumerable() 返回 true)和自有Object.hasOwnProperty() 返回 true)属性的值从一个或多个源对象复制到目标对象。它将返回修改后的目标对象(请注意这个操作是浅拷贝)。

实现

Object.assign = function(target, ...source) {
    if(target == null) {
        throw new TypeError('Cannot convert undefined or null to object');
    }
    let res = Object(target);
    source.forEach(function(obj) {
        if(obj != null) {
            // for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)
            // hasOwnProperty 方法只考虑对象自身的属性
            for(let key in obj) {
                if(obj.hasOwnProperty(key)) {
                    res[key] = obj[key];
                }
            }
        }
    });
    return res;
}

14. Object.is()

描述Object.is 不会转换被比较的两个值的类型,这点和===更为相似,他们之间也存在一些区别。

  1. NaN=== 中是不相等的,而在 Object.is 中是相等的
  2. +0-0=== 中是相等的,而在 Object.is 中是不相等的

实现:利用 ===

Object.is = function(x, y) {
    if(x === y) {
        // 当前情况下,只有一种情况是特殊的,即 +0 -0
        // 如果 x !== 0,则返回true
        // 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断
        return x !== 0 || 1 / x === 1 / y;
    }
    // x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样
    // x和y同时为NaN时,返回true
    return x !== x && y !== y;
}

15. 发布订阅模式(事件总线)

描述:实现一个发布订阅模式,拥有 on, emit, once, off 方法

class EventEmitter {
    constructor() {
        // 包含所有监听器函数的容器对象
    	// 内部结构: {msg1: [listener1, listener2], msg2: [listener3]}
        this.cache = {};
    }
    // 实现订阅
    on(name, callback) {
        if(this.cache[name]) {
            this.cache[name].push(callback);
        }
        else {
            this.cache[name] = [callback];
        }
    }
    // 删除订阅
    off(name, callback) {
        if(this.cache[name]) {
            this.cache[name] = this.cache[name].filter(item => item !== callback);
        }
        if(this.cache[name].length === 0) delete this.cache[name];
    }
    // 只执行一次订阅事件
    once(name, callback) {
        callback();
        this.off(name, callback);
    }
    // 触发事件
    emit(name, ...data) {
        if(this.cache[name]) {
            // 创建副本,如果回调函数内继续注册相同事件,会造成死循环
            let tasks = this.cache[name].slice();
            for(let fn of tasks) {
                fn(...data);
            }
        }
    }
}

16. 实现 JSONP 跨域

JSONP 核心原理script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求;

实现

const jsonp = (url, params, callbackName) => {
    const generateUrl = () => {
        let dataSrc = "";
        for(let key in params) {
            if(params.hasOwnProperty(key)) {
                dataSrc += `${key}=${params[key]}&`
            }
        }
        dataSrc += `callback=${callbackName}`;
        return `${url}?${dataSrc}`;
    }
    return new Promise((resolve, reject) => {
        const scriptEle = document.createElement('script');
        scriptEle.src = generateUrl();
        document.body.appendChild(scriptEle);
        window[callbackName] = data => {
            resolve(data);
            document.removeChild(scriptEle);
        }
    });
}

17. AJAX

实现:利用 XMLHttpRequest

// get
const getJSON = (url) => {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest();
        // open 方法用于指定 HTTP 请求的参数: method, url, async(是否异步,默认true)
        xhr.open("GET", url, false);
        xhr.setRequestHeader('Content-Type', 'application/json');
        // onreadystatechange 属性指向一个监听函数。
        // readystatechange 事件发生时(实例的readyState属性变化),就会执行这个属性。
        xhr.onreadystatechange = function(){
            // 4 表示服务器返回的数据已经完全接收,或者本次接收已经失败
            if(xhr.readyState !== 4) return;
            // 请求成功,基本上只有2xx和304的状态码,表示服务器返回是正常状态
            if(xhr.status === 200 || xhr.status === 304) {
                // responseText 属性返回从服务器接收到的字符串
                resolve(xhr.responseText);
            }
            // 请求失败
            else {
                reject(new Error(xhr.responseText));
            }
        }
        xhr.send();
    });
}

// post
const postJSON = (url, data) => {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest();
        xhr.open("POST", url);
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        xhr.onreadystatechange = function(){
            if(xhr.readyState !== 4) return;
            if(xhr.status === 200 || xhr.status === 304) {
                resolve(xhr.responseText);
            }
            else {
                reject(new Error(xhr.responseText));
            }
        }
        xhr.send(data);
    });
}

18. Promise

Promise的实现参考如下链接

  1. 从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节
  2. 手把手一行一行代码教你“手写Promise“,完美通过 Promises/A+ 官方872个测试用例

18.1 Promise.resolve

Promise.resolve = function(value) {
    // 1.如果 value 参数是一个 Promise 对象,则原封不动返回该对象
    if(value instanceof Promise) return value;
    // 2.如果 value 参数是一个具有 then 方法的对象,则将这个对象转为 Promise 对象,并立即执行它的then方法
    if(typeof value === "object" && 'then' in value) {
        return new Promise((resolve, reject) => {
           value.then(resolve, reject);
        });
    }
    // 3.否则返回一个新的 Promise 对象,状态为 fulfilled
	return new Promise(resolve => resolve(value));
}

18.2 Promise.reject

Promise.reject = function(reason) {
    return new Promise((resolve, reject) => reject(reason));
}

18.3 Promise.all

描述:所有 promise 的状态都变成 fulfilled,就会返回一个状态为 fulfilled 的数组(所有promisevalue)。只要有一个失败,就返回第一个状态为 rejectedpromise 实例的 reason

实现

Promise.all = function(promises) {
    return new Promise((resolve, reject) => {
        if(Array.isArray(promises)) {
            if(promises.length === 0) return resolve(promises);
            let result = [];
            let count = 0;
            promises.forEach((item, index) => {
                Promise.resolve(item).then(
                    value => {
                        count++;
                        result[index] = value;
                        if(count === promises.length) resolve(result);
                    }, 
                    reason => reject(reason)
                );
            })
        }
        else return reject(new TypeError("Argument is not iterable"));
    });
}

18.4 Promise.allSettled

描述:等到所有promise都返回结果,就返回一个promise实例。

实现

Promise.allSettled = function(promises) {
    return new Promise((resolve, reject) => {
        if(Array.isArray(promises)) {
            if(promises.length === 0) return resolve(promises);
            let result = [];
            let count = 0;
            promises.forEach((item, index) => {
                Promise.resolve(item).then(
                    value => {
                        count++;
                        result[index] = {
                            status: 'fulfilled',
                            value: value
                        };
                        if(count === promises.length) resolve(result);
                    }, 
                    reason => {
                        count++;
                        result[index] = {
                            status: 'rejected'.
                            reason: reason
                        };
                        if(count === promises.length) resolve(result);
                    }
                );
            });
        }
        else return reject(new TypeError("Argument is not iterable"));
    });
}

18.5 Promise.race

描述:只要promises中有一个率先改变状态,就返回这个率先改变的Promise实例的返回值。

实现

Promise.race = function(promises){
    return new Promise((resolve, reject) => {
        if(Array.isArray(promises)) {
            if(promises.length === 0) return resolve(promises);
            promises.forEach((item) => {
                Promise.resolve(item).then(
                    value => resolve(value), 
                    reason => reject(reason)
                );
            })
        }
        else return reject(new TypeError("Argument is not iterable"));
    });
}

18.6 Promise.any

描述:只要 promises 中有一个fulfilled,就返回第一个fulfilledPromise实例的返回值。

实现

Promise.any = function(promises) {
    return new Promise((resolve, reject) => {
        if(Array.isArray(promises)) {
            if(promises.length === 0) return reject(new AggregateError("All promises were rejected"));
            let count = 0;
            promises.forEach((item, index) => {
                Promise.resolve(item).then(
                    value => resolve(value),
                    reason => {
                        count++;
                        if(count === promises.length) {
                            reject(new AggregateError("All promises were rejected"));
                        };
                    }
                );
            })
        }
        else return reject(new TypeError("Argument is not iterable"));
    });
}

19. 异步任务调度器

描述:实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有 limit 个。

实现

class Scheduler {
    queue = [];  // 用队列保存正在执行的任务
    runCount = 0;  // 计数正在执行的任务个数
    constructor(limit) {
        this.maxCount = limit;  // 允许并发的最大个数
    }
    add(time, data){
        const promiseCreator = () => {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    console.log(data);
                    resolve();
                }, time);
            });
        }
        this.queue.push(promiseCreator);
        // 每次添加的时候都会尝试去执行任务
        this.request();
    }
    request() {
        // 队列中还有任务才会被执行
        if(this.queue.length && this.runCount < this.maxCount) {
            this.runCount++;
            // 执行先加入队列的函数
            this.queue.shift()().then(() => {
                this.runCount--;
                // 尝试进行下一次任务
                this.request();
            });
        }
    }
}

// 测试
const scheduler = new Scheduler(2);
const addTask = (time, data) => {
    scheduler.add(time, data);
}

addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 输出结果 2 3 1 4

20. setTimeout 模拟 setInterval

描述:使用setTimeout模拟实现setInterval的功能。

实现

const mySetInterval(fn, time) {
    let timer = null;
    const interval = () => {
        timer = setTimeout(() => {
            fn();  // time 时间之后会执行真正的函数fn
            interval();  // 同时再次调用interval本身
        }, time)
    }
    interval();  // 开始执行
    // 返回用于关闭定时器的函数
    return () => clearTimeout(timer);
}

// 测试
const cancel = mySetInterval(() => console.log(1), 400);
setTimeout(() => {
    cancel();
}, 1000);  
// 打印两次1

21. setInterval 模拟 setTimeout

描述:使用setInterval模拟实现setTimeout的功能。

思路setTimeout的特性是在指定的时间内只执行一次,我们只要在setInterval内部执行 callback 之后,把定时器关掉即可。

实现

const mySetTimeout = (fn, time) => {
    let timer = null;
    timer = setInterval(() => {
        // 关闭定时器,保证只执行一次fn,也就达到了setTimeout的效果了
        clearInterval(timer);
        fn();
    }, time);
    // 返回用于关闭定时器的方法
    return () => clearInterval(timer);
}

// 测试
const cancel = mySetTimeout(() => {
    console.log(1);
}, 1000);  
// 一秒后打印 1

22. 图片懒加载

实现getBoundClientRect 的实现方式,监听 scroll 事件(建议给监听事件添加节流),图片加载完会从 img 标签组成的 DOM 列表中删除,最后所有的图片加载完毕后需要解绑监听事件。

// scr 加载默认图片,data-src 保存实施懒加载后的图片
// <img src="./default.jpg" data-src="https://xxx.jpg" alt="" />
let imgs = [...document.querySelectorAll("img")];
const len = imgs.length;

let lazyLoad = function() {
    let count = 0;
    let deleteImgs = [];
    // 获取当前可视区的高度
    let viewHeight = document.documentElement.clientHeight;
    // 获取当前滚动条的位置(距离顶部的距离,等价于document.documentElement.scrollTop)
    let scrollTop = window.pageYOffset;
    imgs.forEach((img) => {
        // 获取元素的大小,及其相对于视口的位置,如 bottom 为元素底部到网页顶部的距离
        let bound = img.getBoundingClientRect();
        // 当前图片距离网页顶部的距离
        // let imgOffsetTop = img.offsetTop;
        
        // 判断图片是否在可视区内,如果在就加载(两种判断方式)
        // if(imgOffsetTop < scrollTop + viewHeight) 
        if (bound.top < viewHeight) {
            img.src = img.dataset.src;  // 替换待加载的图片 src
            count++;
            deleteImgs.push(img);
            // 最后所有的图片加载完毕后需要解绑监听事件
            if(count === len) {
                document.removeEventListener("scroll", imgThrottle);
            }
        }
    });
    // 图片加载完会从 `img` 标签组成的 DOM 列表中删除
    imgs = imgs.filter((img) => !deleteImgs.includes(img));
}

window.onload = function () {
    lazyLoad();
};
// 使用 防抖/节流 优化一下滚动事件
let imgThrottle = debounce(lazyLoad, 1000);
// 监听 `scroll` 事件
window.addEventListener("scroll", imgThrottle);

23. 虚拟DOM转换成真实DOM

描述:将如下 JSON格式的虚拟DOM结构转换成真实DOM结构。

// vnode 结构
{
    tag: 'DIV',
    attrs: {
        id: "app"
    },
    children: [
        {
            tag: 'SPAN',
            children: [
                {
                    tag: 'A',
                    children: []
                }
            ]
        }
    ]
}
// 真实DOM 结构
<div id="app">
    <span>
        <a></a>
    </span>
</div>

实现

function _render(vnode) {
    // 如果是数字类型转化为字符串;
    if(typeof vnode === "number") {
        vnode = String(vnode);
    }
    // 字符串类型直接就是文本节点
    if(typeof vnode === "string") {
        return document.createTextNode(vnode);
    }
    // 普通 DOM
    const dom = document.createElement(vnode.tag);
    if(vnode.attrs) {
        // 遍历属性
        Object.keys(vnode.attrs).forEach((key) => {
            dom.setAttribute(key, vnode.attrs[key]);
        });
    }
    // 子数组进行递归操作
    vnode.children.forEach((child) => dom.appendChild(_render(child)));
    return dom;
}

// 测试
let vnode = {
    tag: "DIV",
    attrs: {
        id: "app",
    },
    children: [
        {
            tag: "SPAN",
            children: [
                {
                    tag: "A",
                    children: [],
                },
            ],
        },
    ],
};
console.log(_render(vnode)); // <div id="app"><span><a></a></span></div>

24. 实现模板字符串解析

描述:实现函数使得将 template 字符串中的{{}}内的变量替换。

核心:使用字符串替换方法 str.replace(regexp|substr, newSubStr|function),使用正则匹配代换字符串。

实现

function render(template, data) {
    // 模板字符串正则 /\{\{(\w+)\}\}/, 加 g 为全局匹配模式, 每次匹配都会调用后面的函数
    let computed = template.replace(/\{\{(\w+)\}\}/g, function(match, key) {
        // match: 匹配的子串;  key:括号匹配的字符串
        return data[key];
    });
    return computed;
}

// 测试
let template = "我是{{name}},年龄{{age}},性别{{sex}}";
let data = {
  name: "张三",
  age: 18
}
console.log(render(template, data)); // 我是张三,年龄18,性别undefined

25. 单例模式

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当您想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

实现

var Singleton = (function() {
    // 如果在内部声明 SingletonClass 对象,则无法在外部直接调用
    var SingletonClass = function() { }; 
    var instance;
    return function() {
        // 如果已存在,则返回 instance
        if(instance) return instance;
        // 如果不存在,则new 一个 SingletonClass 对象
        instance = new SingletonClass();
        return instance;
    }
})();

// 测试
var a = new Singleton();
var b = new Singleton();
console.log(a === b);  // true

参考链接

  1. 最全的手写JS面试题
  2. 32个手写JS,巩固你的JS基础(面试高频)
  3. 死磕 36 个 JS 手写题(搞懂后,提升真的大)
  4. 【中高级前端】必备,30+高频手写实现及详细答案(万字长文),你值得拥有噢