08-手写代码篇

1. JS基础

1. 手写Object.create

思路:将传入的对象作为原型

function create(obj) {
    function F() {
    }

    F.prototype = obj
    return new F()
}

2. 手写instanceof

instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置

实现步骤:

  1. 首先获取类型的原型
  2. 然后获得对象的原型
  3. 然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null
function myInstanceof(left, right) {
    let proto = Object.getPrototypeOf(left), 	// 获取对象的原型
        prototype = right.prototype; 			// 获取构造函数的 prototype 对象

    // 判断构造函数的 prototype 对象是否在对象的原型链上
    while (true) {
        if (!proto) return false;
        if (proto === prototype) return true;

        proto = Object.getPrototypeOf(proto);
    }
}

3. 手写new操作符

在调用 new 的过程中会发生以上四件事情:

  1. 首先创建了一个新的空对象
  2. 设置原型,将对象的原型设置为函数的 prototype 对象
  3. 让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
function objectFactory() {
    let newObject = null;
    let constructor = Array.prototype.shift.call(arguments);
    let result = null;
    // 判断参数是否是一个函数
    if (typeof constructor !== "function") {
        console.error("type error");
        return;
    }
    // 新建一个空对象,对象的原型为构造函数的 prototype 对象
    newObject = Object.create(constructor.prototype);
    // 将 this 指向新建对象,并执行函数
    result = constructor.apply(newObject, arguments);
    // 判断返回对象
    let flag = result && (typeof result === "object" || typeof result === "function");
    // 判断返回结果
    return flag ? result : newObject;
}

// 使用方法
objectFactory(构造函数, 初始化参数);

4. 手写Promise

const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";

function MyPromise(fn) {
    // 保存初始化状态
    var self = this;

    // 初始化状态
    this.state = PENDING;

    // 用于保存 resolve 或者 rejected 传入的值
    this.value = null;

    // 用于保存 resolve 的回调函数
    this.resolvedCallbacks = [];

    // 用于保存 reject 的回调函数
    this.rejectedCallbacks = [];

    // 状态转变为 resolved 方法
    function resolve(value) {
        // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变
        if (value instanceof MyPromise) {
            return value.then(resolve, reject);
        }

        // 保证代码的执行顺序为本轮事件循环的末尾
        setTimeout(() => {
            // 只有状态为 pending 时才能转变,
            if (self.state === PENDING) {
                // 修改状态
                self.state = RESOLVED;

                // 设置传入的值
                self.value = value;

                // 执行回调函数
                self.resolvedCallbacks.forEach(callback => {
                    callback(value);
                });
            }
        }, 0);
    }

    // 状态转变为 rejected 方法
    function reject(value) {
        // 保证代码的执行顺序为本轮事件循环的末尾
        setTimeout(() => {
            // 只有状态为 pending 时才能转变
            if (self.state === PENDING) {
                // 修改状态
                self.state = REJECTED;

                // 设置传入的值
                self.value = value;

                // 执行回调函数
                self.rejectedCallbacks.forEach(callback => {
                    callback(value);
                });
            }
        }, 0);
    }

    // 将两个方法传入函数执行
    try {
        fn(resolve, reject);
    } catch (e) {
        // 遇到错误时,捕获错误,执行 reject 函数
        reject(e);
    }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
    // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
    onResolved =
        typeof onResolved === "function"
            ? onResolved
            : function (value) {
                return value;
            };

    onRejected =
        typeof onRejected === "function"
            ? onRejected
            : function (error) {
                throw error;
            };

    // 如果是等待状态,则将函数加入对应列表中
    if (this.state === PENDING) {
        this.resolvedCallbacks.push(onResolved);
        this.rejectedCallbacks.push(onRejected);
    }

    // 如果状态已经凝固,则直接执行对应状态的函数
    if (this.state === RESOLVED) {
        onResolved(this.value);
    }

    if (this.state === REJECTED) {
        onRejected(this.value);
    }
};

5. 手写Promise.then

then 方法返回一个新的 promise 实例,为了在 promise 状态发生变化时(resolve / reject 被调用时)再执行 then 里的函数,使用一个 callbacks 数组先把传给 then 的函数暂存起来,等状态改变时再调用

那么,怎么保证后一个 then 里的方法在前一个 then(可能是异步)结束之后再执行呢?

可以将传给 then 函数和新 promiseresolve 一起 push 到前一个 promisecallbacks 数组中,达到承前启后的效果:

  • 承前:当前一个 promise 完成后,调用其 resolve 变更状态,在这个 resolve 里会依次调用 callbacks 里的回调,这样就执行了 then 里的方法了
  • 启后:上一步中,当 then 里的方法执行完成后,返回一个结果,如果这个结果是个简单的值,就直接调用新 promiseresolve,让其状态变更,这又会依次调用新 promisecallbacks 数组里的方法,循环往复。如果返回的结果是个 promise,则需要等它完成之后再触发新 promiseresolve,所以可以在其结果的 then 里调用新 promiseresolve
then(onFulfilled, onReject)
{
    // 保存前一个promise的this
    const self = this;
    return new MyPromise((resolve, reject) => {
        // 封装前一个promise成功时执行的函数
        let fulfilled = () => {
            try {
                const result = onFulfilled(self.value); // 承前
                return result instanceof MyPromise ? result.then(resolve, reject) : resolve(result); //启后
            } catch (err) {
                reject(err)
            }
        }
        // 封装前一个promise失败时执行的函数
        let rejected = () => {
            try {
                const result = onReject(self.reason);
                return result instanceof MyPromise ? result.then(resolve, reject) : reject(result);
            } catch (err) {
                reject(err)
            }
        }
        switch (self.status) {
            case PENDING:
                self.onFulfilledCallbacks.push(fulfilled);
                self.onRejectedCallbacks.push(rejected);
                break;
            case FULFILLED:
                fulfilled();
                break;
            case REJECT:
                rejected();
                break;
        }
    })
}

注意:

  • 连续多个 then 里的回调方法是同步注册的,但注册到了不同的 callbacks 数组中,因为每次 then 都返回新的 promise 实例(参考上面的例子和图)
  • 注册完成后开始执行构造函数中的异步事件,异步完成之后依次调用 callbacks 数组中提前注册的回调

6. 手写 Promise.all

  1. 核心思路
    1. 接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数
    2. 这个方法返回一个新的 promise 对象
    3. 遍历传入的参数,用 Promise.resolve() 将参数"包一层",使其变成一个 promise 对象
    4. 参数所有回调成功才是成功,返回值数组与参数顺序一致
    5. 参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息
  2. 实现代码
    • 一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了
function promiseAll(promises) {
    return new Promise(function (resolve, reject) {
        if (!Array.isArray(promises)) {
            throw new TypeError(`argument must be a array`)
        }
        var resolvedCounter = 0;
        var promiseNum = promises.length;
        var resolvedResult = [];
        for (let i = 0; i < promiseNum; i++) {
            Promise.resolve(promises[i]).then(value => {
                resolvedCounter++;
                resolvedResult[i] = value;
                if (resolvedCounter == promiseNum) {
                    return resolve(resolvedResult)
                }
            }, error => {
                return reject(error)
            })
        }
    })
}

// test
let p1 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(1)
    }, 1000)
})
let p2 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(2)
    }, 2000)
})
let p3 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(3)
    }, 3000)
})
promiseAll([p3, p1, p2]).then(res => {
    console.log(res) // [3, 1, 2]
})

7. 手写Promise.race

  • 该方法的参数是 Promise 实例数组,然后其 then 注册的回调方法是数组中的某一个 Promise 的状态变为 fulfilled 的时候就执行。因为 Promise 的状态只能改变一次,那么我们只需要把 Promise.race 中产生的 Promise 对象的 resolve 方法,注入到数组中的每一个 Promise 实例中的回调函数中即可
Promise.race = function (args) {
    return new Promise((resolve, reject) => {
        for (let i = 0, len = args.length; i < len; i++) {
            args[i].then(resolve, reject)
        }
    })
}

8. 手写防抖函数

  • 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求
// 函数防抖的实现
function debounce(fn, wait) {
    let timer = null;

    return function () {
        let context = this,
            args = arguments;

        // 如果此时存在定时器的话,则取消之前的定时器重新记时
        if (timer) {
            clearTimeout(timer);
            timer = null;
        }

        // 设置定时器,使事件间隔指定事件后执行
        timer = setTimeout(() => {
            fn.apply(context, args);
        }, wait);
    };
}

9. 手写节流函数

  • 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率
// 函数节流的实现;
function throttle(fn, delay) {
    let curTime = Date.now();

    return function () {
        let context = this,
            args = arguments,
            nowTime = Date.now();

        // 如果两次时间间隔超过了指定时间,则执行函数。
        if (nowTime - curTime >= delay) {
            curTime = Date.now();
            return fn.apply(context, args);
        }
    };
}

10. 手写类型判断函数

function getType(value) {
    // 判断数据是 null 的情况
    if (value === null) {
        return value + "";
    }
    // 判断数据是引用类型的情况
    if (typeof value === "object") {
        let valueClass = Object.prototype.toString.call(value),
            type = valueClass.split(" ")[1].split("");
        type.pop();
        return type.join("").toLowerCase();
    } else {
        // 判断数据是基本数据类型的情况和函数的情况
        return typeof value;
    }
}

11. 手写 call 函数

call 函数的实现步骤:

  1. 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window
  3. 处理传入的参数,截取第一个参数后的所有参数
  4. 将函数作为上下文对象的一个属性
  5. 使用上下文对象来调用这个方法,并保存返回结果
  6. 删除刚才新增的属性
  7. 返回结果
// call函数实现
Function.prototype.myCall = function (context) {
    // 判断调用对象
    if (typeof this !== "function") {
        console.error("type error");
    }
    // 获取参数
    let args = [...arguments].slice(1),
        result = null;
    // 判断 context 是否传入,如果未传入则设置为 window
    context = context || window;
    // 将调用函数设为对象的方法
    context.fn = this;
    // 调用函数
    result = context.fn(...args);
    // 将属性删除
    delete context.fn;
    return result;
};

12. 手写 apply 函数

apply 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window
  3. 将函数作为上下文对象的一个属性
  4. 判断参数值是否传入
  5. 使用上下文对象来调用这个方法,并保存返回结果
  6. 删除刚才新增的属性
  7. 返回结果
// apply 函数实现
Function.prototype.myApply = function (context) {
    // 判断调用对象是否为函数
    if (typeof this !== "function") {
        throw new TypeError("Error");
    }
    let result = null;
    // 判断 context 是否存在,如果未传入则为 window
    context = context || window;
    // 将函数设为对象的方法
    context.fn = this;
    // 调用方法
    if (arguments[1]) {
        result = context.fn(...arguments[1]);
    } else {
        result = context.fn();
    }
    // 将属性删除
    delete context.fn;
    return result;
};

13. 手写bind函数

bind 函数的实现步骤:

  1. 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况
  2. 保存当前函数的引用,获取其余传入参数值
  3. 创建一个函数返回
  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象

2. 数据处理

1. 实现日期格式化函数

dateFormat(new Date('2020-12-01'), 'yyyy/MM/dd') 	// 2020/12/01
dateFormat(new Date('2020-04-01'), 'yyyy/MM/dd') 	// 2020/04/01
dateFormat(new Date('2020-04-01'), 'yyyy年MM月dd日') // 2020年04月01日
const dateFormat = (dateInput, format) => {
    var day = dateInput.getDate()
    var month = dateInput.getMonth() + 1
    var year = dateInput.getFullYear()
    format = format.replace(/yyyy/, year)
    format = format.replace(/MM/, month)
    format = format.replace(/dd/, day)
    return format
}

2. 交换a,b的值

巧妙的利用两个数的和、差(不能用临时变量):

a = a + b
b = a - b
a = a - b

3. 实现数组乱序输出

实现思路:

  • 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行交换
  • 第二次取出数据数组第二个元素,随机产生一个除了索引为1的之外的索引值,并将第二个元素与该索引值对应的元素进行交换
  • 按照上面的规律执行,直到遍历完成
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (var i = 0; i < arr.length; i++) {
    const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
    [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
console.log(arr)

倒序遍历:

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let length = arr.length,
    randomIndex,
    temp;
while (length) {
    randomIndex = Math.floor(Math.random() * length--);
    temp = arr[length];
    arr[length] = arr[randomIndex];
    arr[randomIndex] = temp;
}
console.log(arr)

4. 实现数组元素求和

let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let sum = arr.reduce((total, i) => total += i, 0);
console.log(sum);
var arr = [1, 2, 3, [[4, 5], 6], 7, 8, 9];
let arr = arr.toString().split(',').reduce((total, i) => total += Number(i), 0);
console.log(arr);
// 递归实现:
let arr = [1, 2, 3, 4, 5, 6]

function add(arr) {
    if (arr.length == 1) return arr[0]
    return arr[0] + add(arr.slice(1))
}

console.log(add(arr)) // 21

5. 实现数组的扁平化

  1. 递归实现
    • 普通的递归思路,就是通过循环递归的方式,一项一项地去遍历,如果每一项还是一个数组,那么就继续往下遍历,利用递归程序的方法,来实现数组的每一项的连接:
let arr = [1, [2, [3, 4, 5]]];

function flatten(arr) {
    let result = [];

    for (let i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]));
        } else {
            result.push(arr[i]);
        }
    }
    return result;
}

flatten(arr);  //  [1, 2, 3, 4,5]
  1. reduce函数迭代
    • 从上面普通的递归函数中可以看出,其实就是对数组的每一项进行处理,那么其实也可以用 reduce 来实现数组的拼接,从而简化第一种方法的代码,改造后的代码:
let arr = [1, [2, [3, 4]]];

function flatten(arr) {
    return arr.reduce(function (prev, next) {
        return prev.concat(Array.isArray(next) ? flatten(next) : next)
    }, [])
}

console.log(flatten(arr));//  [1, 2, 3, 4,5]
  1. 扩展运算符实现
    • 采用了扩展运算符和 some 的方法,两者共同使用,达到数组扁平化的目的:
let arr = [1, [2, [3, 4]]];

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

console.log(flatten(arr)); //  [1, 2, 3, 4,5]

3. 场景应用

1. 循环打印红黄绿

对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?

// 三个亮灯函数:
function red() {
    console.log('red');
}

function green() {
    console.log('green');
}

function yellow() {
    console.log('yellow');
}

1. 用callback实现

const task = (timer, light, callback) => {
    setTimeout(() => {
        if (light === 'red') {
            red()
        } else if (light === 'green') {
            green()
        } else if (light === 'yellow') {
            yellow()
        }
        callback()
    }, timer)
}

task(3000, 'red', () => {
    task(2000, 'green', () => {
        task(1000, 'yellow', Function.prototype)
    })
})
  • 这里存在一个 bug:代码只是完成了一次流程,执行后红黄绿灯分别只亮一次。该如何让它交替重复进行呢?
  • 上面提到过递归,可以递归亮灯的一个周期:
// 注意看黄灯亮的回调里又再次调用了 step 方法,以完成循环亮灯
const step = () => {
    task(3000, 'red', () => {
        task(2000, 'green', () => {
            task(1000, 'yellow', step)
        })
    })
}

step()

2. 用promise实现

const task = (timer, light) =>
    new Promise((resolve, reject) => {
        setTimeout(() => {
            if (light === 'red') {
                red()
            } else if (light === 'green') {
                green()
            } else if (light === 'yellow') {
                yellow()
            }
            resolve()
        }, timer)
    })

const step = () => {
    task(3000, 'red')
        .then(() => task(2000, 'green'))
        .then(() => task(2100, 'yellow'))
        .then(step)
}

step()
  • 这里将回调移除,在一次亮灯结束后,resolve 当前 promise,并依然使用递归进行

3. 用async/await实现

const taskRunner = async () => {
    await task(3000, 'red')
    await task(2000, 'green')
    await task(2100, 'yellow')
    taskRunner()
}

taskRunner()

2. 每隔一秒打印 1,2,3,4

// 使用闭包实现
for (var i = 0; i < 5; i++) {
    (function (i) {
        setTimeout(function () {
            console.log(i);
        }, i * 1000);
    })(i);
}

// 使用 let 块级作用域
for (let i = 0; i < 5; i++) {
    setTimeout(function () {
        console.log(i);
    }, i * 1000);
}

3. 小孩报数问题

  • 有 30 个小孩儿,编号从 1-30,围成一圈依此报数,1、2、3 数到 3 的小孩儿退出这个圈, 然后下一个小孩重新报数 1、2、3,问最后剩下的那个小孩儿的编号是多少?
function childNum(num, count) {
    let allplayer = [];
    for (let i = 0; i < num; i++) {
        allplayer[i] = i + 1;
    }

    let exitCount = 0;    // 离开人数
    let counter = 0;      // 记录报数
    let curIndex = 0;     // 当前下标

    while (exitCount < num - 1) {
        if (allplayer[curIndex] !== 0) counter++;

        if (counter == count) {
            allplayer[curIndex] = 0;
            counter = 0;
            exitCount++;
        }
        curIndex++;
        if (curIndex == num) {
            curIndex = 0
        }
        ;
    }
    for (i = 0; i < num; i++) {
        if (allplayer[i] !== 0) {
            return allplayer[i]
        }
    }
}

childNum(30, 3)

4. Promise实现图片异步加载

let imageAsync = (url) => {
    return new Promise((resolve, reject) => {
        let img = new Image();
        img.src = url;
        img.οnlοad = () => {
            console.log(`图片请求成功,此处进行通用操作`);
            resolve(image);
        }
        img.οnerrοr = (err) => {
            console.log(`失败,此处进行失败的通用操作`);
            reject(err);
        }
    })
}

imageAsync("url").then(() => {
    console.log("加载成功");
}).catch((error) => {
    console.log("加载失败");
})

5. 实现发布-订阅模式