Skip to content

手写 JS

手写类型判断

js
const myTypeOf = (data) => Object.prototype.toString.call(data).slice(8, -1).toLowerCase()

// 测试
console.log(myTypeOf(1)) //--> number
console.log(myTypeOf('1')) //--> string
console.log(myTypeOf(true)) //--> boolean
console.log(myTypeOf([])) //--> array
console.log(myTypeOf({})) //--> object
console.log(myTypeOf(/^/)) //--> regexp
console.log(myTypeOf(new Date())) //--> date
console.log(myTypeOf(Math)) //--> math
console.log(myTypeOf(() => {})) //--> function

手写数组去重

js
const myUnique = array => [...new Set(array)]

// 测试
console.log(myUnique([1, 1, 2, 3])) //--> [1, 2, 3]

手写 Ajax 的 GET 方法

js
function myAjaxGet(url) {
  // 创建一个 Promise 对象
  return new Promise(function(resolve, reject) {
    const xhr = new XMLHttpRequest()

    // 新建一个 http 请求
    xhr.open('GET', url, true)

    // 设置响应的数据类型
    xhr.responseType = 'json'

    // 设置请求头信息
    xhr.setRequestHeader('Accept', 'application/json')

    // 设置状态的监听函数
    xhr.onreadystatechange = function() {
      if (this.readyState !== 4) return

      // 当请求成功或失败时,改变 Promise 的状态
      if (this.status === 200) {
        resolve(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }

    // 设置错误监听函数
    xhr.onerror = function() {
      reject(new Error(this.statusText))
    }

    // 发送 http 请求
    xhr.send(null)
  })
}

// 测试
myAjaxGet('https://api.github.com/users/XPoet').then(res => {
  console.log('res: ', res) //--> {...}
})

手写深浅拷贝

浅拷贝

js
function shallowCopy(object) {
  // 只拷贝对象类型的数据
  if (!object || typeof object !== 'object') return

  // object 如果是数组类型就新建一个空数组,否则新建空对象
  const newObject = Array.isArray(object) ? [] : {}

  // 遍历 object,进行属性拷贝
  for (const key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key]
    }
  }

  return newObject
}

// 测试
const obj1 = { x: 1, y: 2, z: 3 }
const obj2 = shallowCopy(obj1)
console.log(obj2) //--> { x: 1, y: 2, z: 3 }

const arr1 = [1, 2, 3]
const arr2 = shallowCopy(arr1)
console.log(arr2) //--> [1, 2, 3]

深拷贝

js
function deepCopy(object) {
  // 只拷贝对象类型的数据
  if (!object || typeof object !== 'object') return

  // object 如果是数组类型就新建一个空数组,否则新建空对象
  const newObject = Array.isArray(object) ? [] : {}

  for (const key in object) {
    if (object.hasOwnProperty(key)) {
      // object[key] 如果是对象类型,则使用递归继续遍历拷贝属性
      newObject[key] = typeof object[key] === 'object' ? deepCopy(object[key]) : object[key]
    }
  }

  return newObject
}

// 测试
const obj1 = { x: 1, y: { z: 3 } }
const obj2 = deepCopy(obj1)
console.log(obj2) //--> { x: 1, y: { z: 3 } }

const arr1 = [1, [2, 3]]
const arr2 = deepCopy(arr1)
console.log(arr2) //--> [1, [2, 3]]

手写 call、apply 和 bind 函数

在 JavaScript 中,callapplybindFunction 对象自带的三个方法,这三个方法的主要作用是改变函数中的 this 指向。

共同点:

  • applycallbind 三者都是用来改变函数的 this 对象指向。
  • applycallbind 三者第一个参数都是 this 要指向的对象,也就是想指定的上下文。(函数的每次调用都会拥有一个特殊值——本次调用的上下文(context),这就是 this 关键字的值。)
  • applycallbind 三者都可以利用后续参数传参。

区别:

  • bind 是返回对应函数,便于稍后调用。
  • applycall 则是立即调用。

call()apply() 的作用是一样的,都是用于改变 this 的指向,区别在于 call 接受多个参数,而 apply 接受的是一个数组。

第一个参数的取值有以下 4 种情况:

  1. 不传,或者传 nullundefined,函数中的 this 指向 window 对象。
  2. 传递另一个函数的函数名,函数中的 this 指向这个函数的引用。
  3. 传递字符串、数值或布尔类型等基础类型,函数中的 this 指向其对应的包装对象,如 StringNumberBoolean
  4. 传递一个对象,函数中的 this 指向这个对象。
js
function a() {
  console.log(this)
}

function b() {}

const c = { x: 1 }

a.call() //--> window
a.call(null) //--> window
a.call(undefined) //window
a.call(1) //--> Number {1}
a.call('') //--> String {''}
a.call(true) //--> Boolean {true}
a.call(b) //--> function b(){}
a.call(c) //--> {x: 1}

手写 call

call 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window
  3. 将函数作为上下文对象的一个属性。
  4. 使用上下文对象来调用这个方法,并保存返回结果。
  5. 删除刚才新增的属性。
  6. 返回结果。
js
Function.prototype.myCall = function(ctx, ...args) {
  // 判断调用对象
  if (typeof this !== 'function') {
    throw new TypeError('Type Error')
  }

  // 判断 ctx 是否传入,如果未传入则设置为 window
  ctx = ctx || window

  // 将调用函数设为对象的方法
  ctx.fn = this

  // 调用函数
  const result = ctx.fn(...args)

  // 将属性删除
  delete ctx.fn

  return result
}


// 测试
const obj = {
  test(a, b, c) {
    console.log(this, a, b)
  }
}
obj.test.myCall(obj, 4, 5) //--> {test: ƒ, fn: ƒ} 4 5

手写 apply

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

  let result = null

  // 判断 ctx 是否存在,如果未传入则为 window
  ctx = ctx || window

  // 将函数设为对象的方法
  ctx.fn = this

  // 调用方法
  if (arguments[1]) {
    result = ctx.fn(...arguments[1])
  } else {
    result = ctx.fn()
  }

  // 将属性删除
  delete ctx.fn

  return result
}

// 测试
const obj = {
  test(a, b, c) {
    console.log(this, a, b, c)
  }
}
obj.test.myApply(obj, [4, 5, 6]) //--> {test: ƒ, fn: ƒ} 4 5 6

手写 bind

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 保存当前函数的引用,获取其余传入参数值。
  3. 创建一个函数返回。
  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 thisapply 调用,其余情况都传入指定的上下文对象。
js
Function.prototype.myBind = function(ctx, ...args) {
  // 判断调用对象是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('Type Error')
  }

  const fn = this

  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : ctx, args.concat(...arguments)
    )
  }
}

// 测试
const obj = {
  test(a, b, c) {
    console.log(this, a + b)
  }
}
obj.test.myBind(obj, 4, 5)() //--> {test: ƒ} 9

函数柯里化

函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。例如:add(1, 2, 3, 4, 5) 转换成 add(1)(2)(3)(4)(5)

js
function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args)
}

// 测试
// 普通函数
function fn(a, b, c, d, e) {
  console.log(a, b, c, d, e)
}

// 生成的柯里化函数
const _fn = curry(fn)

_fn(1, 2, 3, 4, 5) //--> 1 2 3 4 5
_fn(1)(2)(3, 4, 5) //--> 1 2 3 4 5
_fn(1, 2)(3, 4)(5) //--> 1 2 3 4 5
_fn(1)(2)(3)(4)(5) //--> 1 2 3 4 5

看起来柯里化好像是把简答的问题复杂化了,但是复杂化的同时,我们在使用函数时拥有了更加多的自由度。而这里对于函数参数的自由处理,正是柯里化的核心所在。柯里化本质上是降低通用性,提高适用性。

手写 EventBus

javascript
class EventBus {
    constructor() {
        // 存储事件及其对应的回调函数
        this.events = {};
    }

    // 订阅事件
    subscribe(eventName, callback) {
        // 如果事件不存在,则创建一个新的事件数组
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        // 将回调函数添加到事件数组中
        this.events[eventName].push(callback);
    }

    // 取消订阅事件
    unsubscribe(eventName, callback) {
        // 如果事件不存在,则直接返回
        if (!this.events[eventName]) {
            return;
        }
        // 从事件数组中移除指定的回调函数
        this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
    }

    // 发布事件
    next(eventName, data) {
        // 如果事件不存在,则直接返回
        if (!this.events[eventName]) {
            return;
        }
        // 遍历事件数组,依次执行回调函数
        this.events[eventName].forEach(callback => {
            callback(data);
        });
    }
}

// 创建一个新的 EventBus 实例
const eventBus = new EventBus();

// 定义事件处理函数
const handler1 = data => {
    console.log('Handler 1:', data);
};

const handler2 = data => {
    console.log('Handler 2:', data);
};

// 订阅事件
eventBus.subscribe('event1', handler1);
eventBus.subscribe('event1', handler2);

// 发布事件
eventBus.next('event1', 'Hello, EventBus!');

// 取消订阅事件
eventBus.unsubscribe('event1', handler2);

手写 Promise

Promise 是异步编程的一种解决方案,比传统的解决方案回调函数和事件更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。

所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

下面我们用 ES6 语法来手写一个 Promise:

js
class MyPromise {

  // Promise/A+ 规范规定的三种状态
  PENDING = 'pending' // 等待状态
  FULFILLED = 'fulfilled'// 成功状态
  REJECTED = 'rejected' // 失败状态

  // 构造函数接收一个执行回调
  constructor(executor) {
    this._status = this.PENDING // Promise 初始状态
    this._value = undefined // then 回调的值
    this._resolveQueue = [] // resolve 时触发的成功队列
    this._rejectQueue = [] // reject 时触发的失败队列

    // 使用箭头函数固定 this(resolve 函数在 executor 中触发,不然找不到 this)
    const resolve = value => {
      const run = () => {
        // Promise/A+ 规范规定的 Promise 状态只能从 pending 触发,变成 fulfilled
        if (this._status === this.PENDING) {
          this._status = this.FULFILLED // 更改状态
          this._value = value // 储存当前值,用于 then 回调

          // 执行 resolve 回调
          while (this._resolveQueue.length) {
            const callback = this._resolveQueue.shift()
            callback(value)
          }
        }
      }
      // 把 resolve 执行回调的操作封装成一个函数,放进 setTimeout 里,以实现 Promise 异步调用的特性(规范上是微任务,这里是宏任务)
      setTimeout(run)
    }

    // 同 resolve
    const reject = value => {
      const run = () => {
        if (this._status === this.PENDING) {
          this._status = this.REJECTED
          this._value = value

          while (this._rejectQueue.length) {
            const callback = this._rejectQueue.shift()
            callback(value)
          }
        }
      }
      setTimeout(run)
    }

    // new Promise() 时立即执行 executor,并传入 resolve 和 reject
    executor(resolve, reject)
  }

  // then 方法,接收一个成功的回调和一个失败的回调
  then(onFulfilled, onRejected) {
    // 根据规范,如果 then 的参数不是 function,则忽略它,让值继续往下传递,链式调用继续往下执行
    typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
    typeof onRejected !== 'function' ? onRejected = error => error : null

    // then 返回一个新的 Promise
    return new MyPromise((resolve, reject) => {
      const resolveFn = value => {
        try {
          const x = onFulfilled(value)
          // 分类讨论返回值,如果是 Promise,那么等待 Promise 状态变更,否则直接 resolve
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
          reject(error)
        }
      }

      const rejectFn = error => {
        try {
          const x = onRejected(error)
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
          reject(error)
        }
      }

      switch (this._status) {
        case this.PENDING:
          this._resolveQueue.push(resolveFn)
          this._rejectQueue.push(rejectFn)
          break
        case this.FULFILLED:
          resolveFn(this._value)
          break
        case this.REJECTED:
          rejectFn(this._value)
          break
      }
    })
  }

  catch(onRejected) {
    return this.then(null, onRejected)
  }

  finally(callback) {
    return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
      MyPromise.resolve(callback()).then(() => error)
    })
  }

  // 静态 resolve 方法
  static resolve(value) {
    return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
  }

  // 静态 reject 方法
  static reject(error) {
    return new MyPromise((resolve, reject) => reject(error))
  }

  // 静态 all 方法
  static all(promiseArr) {
    let count = 0
    let result = []
    return new MyPromise((resolve, reject) => {
      if (!promiseArr.length) {
        return resolve(result)
      }
      promiseArr.forEach((p, i) => {
        MyPromise.resolve(p).then(value => {
          count++
          result[i] = value
          if (count === promiseArr.length) {
            resolve(result)
          }
        }, error => {
          reject(error)
        })
      })
    })
  }

  // 静态 race 方法
  static race(promiseArr) {
    return new MyPromise((resolve, reject) => {
      promiseArr.forEach(p => {
        MyPromise.resolve(p).then(value => {
          resolve(value)
        }, error => {
          reject(error)
        })
      })
    })
  }
}

// 测试
function fn() {
  return new MyPromise((resolve, reject) => {
    if (Math.random() > 0.5) {
      setTimeout(() => {
        resolve(`resolve ***`)
      }, 500)
    } else {
      setTimeout(() => {
        reject(`reject ***`)
      }, 500)
    }
  })
}

fn().then(res => {
  console.log('resolve value: ', res)
}).catch(err => {
  console.log('reject value: ', err)
})

手写 JSONP

js
function jsonp(url, params, callback) {
  // 判断是否含有参数
  let queryString = url.indexOf('?') === '-1' ? '?' : '&';

  // 添加参数
  for (var k in params) {
    if (params.hasOwnProperty(k)) {
      queryString += k + '=' + params[k] + '&';
    }
  }

  // 处理回调函数名
  let random = Math.random().toString().replace('.', ''),
    callbackName = 'myJsonp' + random;

  // 添加回调函数
  queryString += 'callback=' + callbackName;

  // 构建请求
  let scriptNode = document.createElement('script');
  scriptNode.src = url + queryString;

  window[callbackName] = function () {
    // 调用回调函数
    callback(...arguments);

    // 删除这个引入的脚本
    document.getElementsByTagName('head')[0].removeChild(scriptNode);
  };

  // 发起请求
  document.getElementsByTagName('head')[0].appendChild(scriptNode);
}

手写观察者模式

js
var events = (function () {
  var topics = {};

  return {
    // 注册监听函数
    subscribe: function (topic, handler) {
      if (!topics.hasOwnProperty(topic)) {
        topics[topic] = [];
      }
      topics[topic].push(handler);
    },

    // 发布事件,触发观察者回调事件
    publish: function (topic, info) {
      if (topics.hasOwnProperty(topic)) {
        topics[topic].forEach(function (handler) {
          handler(info);
        });
      }
    },

    // 移除主题的一个观察者的回调事件
    remove: function (topic, handler) {
      if (!topics.hasOwnProperty(topic)) return;

      var handlerIndex = -1;
      topics[topic].forEach(function (item, index) {
        if (item === handler) {
          handlerIndex = index;
        }
      });

      if (handlerIndex >= 0) {
        topics[topic].splice(handlerIndex, 1);
      }
    },

    // 移除主题的所有观察者的回调事件
    removeAll: function (topic) {
      if (topics.hasOwnProperty(topic)) {
        topics[topic] = [];
      }
    }
  };
})();

Released under the AGPL-3.0 License