【JS手写系列】手写一切

106 阅读7分钟

手写 Call

call 的特点:

  1. 如果是null、undefined 默认指向window
  2. 如果是基本数据类型 字符串、数字、布尔值,会被转为包装对象
  3. 第一个参数为要改变的this指向
  4. call函数的返回值是返回被借调函数的返回值
Function.prototype.myCall = function(context, ...args) {
    // 处理 null 和 undefined
    if (context === null || context === undefined) {
        context = window
    }
    // 处理基本数据类型
    if (typeof context !== 'object' && typeof context !== 'function') {
        context = Object(context)
    }
    const fnSymbol = Symbol(); // 使用Symbol以避免属性冲突 
    // 这样在打印的时候不会出现新增的属性,替代下面的这种写法
    Object.defineProperty(context, fnSymbol, {
        enumerable: false,
        value: this
    })
    // context[fnSymbol] = this; 
    const result = context[fnSymbol](...args); 
    delete context[fnSymbol]; 
    return result;
}

手写 apply

Function.prototype.myApply = function(context, ...args) {
    if (context === null || context === undefined) {
       context = window
    }
    // 处理基本数据类型
    if (typeof context !== 'object' && typeof context !== 'function') {
        context = Object(context)
    }
    const fnSymbol = Symbol(); // 使用Symbol以避免属性冲突 
    // 这样在打印的时候不会出现新增的属性,替代下面的这种写法
    Object.defineProperty(context, fnSymbol, {
        enumerable: false,
        value: this
    })
    //context[fnSymbol] = this; 
    // 展开参数传递
    const result = context[fnSymbol](...args[0]); 
    delete context[fnSymbol]; 
    return result;
}

手写 bind

bind的特点:

  1. 返回一个新函数
  2. 并且改变新函数的 this 指向
  3. 返回的新函数可以作为普通函数、可以作为构造函数使用,如果是构造函数使用,则看返回的结果是否是引用数据类型,如果是,则返回该引用类型,否则忽略返回this
  4. 并且新函数也可以传递参数
Function.prototype.myBind = function (context, ...args1) {
    var _this = this
    var boundFunction = function(...args2) {
        // 参数拼接
        const args = args1.concat(args2)
        // 判断是否是 new 调用
        const isNew = this instanceof boundFunction
        const _cxt = isNew ? this : context
        // 原函数的返回值,就是新函数的返回值
        const result = _this.apply(_cxt, args)
        // 如果是构造函数调用并且返回值是引用数据类型
        if (isNew && typeof result === 'object' && result !== null) {
            return result
        }
        // 如果没有返回值但是是new调用就返回this,否则返回result
        return isNew ? this : result
    }
    // 维护原型链,如果绑定的是构造函数,确保new操作符创建的对象实例继承自原函数的原型
    if (this.prototype) {
        boundFunction.prototype = Object.create(this.prototype)
    }   
    return boundFunction
}

// 示例
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function () {
  console.log("Hello, " + this.name);
};

// 使用原函数调用 sayHello 方法
const person1 = new Person("Alice");
person1.sayHello(); // 输出: Hello, Alice

// 使用绑定函数调用 sayHello 方法
const boundFunction = Person.myBind({ name: "Bob" });
const person2 = new boundFunction();
person2.sayHello(); // 如果不处理原型链,则会报错
  • 方式2
Function.prototype.myBind = function (thisArg, ...args) {
  const fn = this
  return function (...args1) {
    const allArgs = [...args, ...args1]
    // 判断是否为new的构造函数
    if (new.target) {
      return new fn(...allArgs)
    } else {
      return fn.call(thisArg, allArgs)
    }
  }
}

手写 new

new 运算符做了哪些事?

  1. 创建一个空对象
  2. 设置原型链:将空对象的隐式原型指向构造函数的显示原型
  3. 绑定this,执行构造函数,将this指向这个新对象
  4. 判断返回值,如果构造函数,则返回这个对象;否则返回创建的空对象
function myNew(fn, ...args) {
   // 1. 创建一个空对象
   var obj = Object.create(fn.prototype)
   // 2. 执行构造函数,并设置 this 指向
   var result = fn.apply(obj, args)
   // 3. 判断返回值类型
   return result instanceof Object ? result : obj
}

手写 instanceof

用于检测构造函数的 prototype 是否出现在某个实例对象的原型链上

function myInstanceof(object, constructor) {
    // 排除基本数据类型
    if (typeof object !== 'object' || !object) {
        return false
    }
    // 获取object的原型对象
    let left = object.__proto__
    let right = constructor.prototype
    while(left) {
        if (left === right) {
            return true
        }
        left = left.__proto__
    }
    return false
}

手写深拷贝

function deepClone(obj) {
    // 1.排除 null、undefined、函数 以及基本数据类型
    if (typeof obj !== 'object' || !obj) {
        return obj
    }
    // {}、[]、Date、Error、RegExp、Arguments...
    // 2.对正则特殊处理
    if (obj instanceof RegExp) {
        return new RegExp(obj)
    }
    let result = Array.isArray(obj) ? [] : new obj.constructor()
    for(const key in result) {
        result[key] = deepClone(obj[key])
    }
    return result
}

防抖

function debounce(fn, delay=200) {
    let timer = null;
    return function() {
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, arguments)
        }, delay)
    }
}

节流

function throttle(fn, delay=200) {
    var startTime = 0
    var timer = null
    return function() {
        var endTime = Date.now()
        if (endTime - startTime > delay) {
            fn.apply(this, arguments)
            startTime = endTime
        } else { // 保证至少触发一次
            clearTimeout(timer)
            timer = setTimeout(() => {
                fn,apply(this, arguments)
            }, delay)
        }
    }
}

冒泡排序

function bubbleSort(arr) {
    var len = arr.length
    for(var i = 0; i < len - 1; i++) { // 轮数
        for(var j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j+1]) {
                var temp = arr[j]
                arr[j] = arr[j+1]
                arr[j+1] = temp
            }
        }
    }
}

选择排序

function selectionSort(arr) {
  const length = arr.length;
  for (let i = 0; i < length - 1; i++) {
    // 定义最小元素的下标
    let minIndex = i;
    for (let j = i + 1; j < length; j++) {
      if (arr[j] < arr[minIndex]) {
        // 找到小的元素就更新下标
        minIndex = j;
      }
    }
    // 交换位置
    if (minIndex !== i) {
      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
  }
  return arr;
}

快速排序

实现思路:

  1. 随机选择一个数作为基准点
  2. 遍历数组,将小于基准值的数放在基准的左边,大于基准值的数放在右边
  3. 递归将小于基准的数组和大于基准的数组进行排序
function quickSort(arr) {
    var len = arr.length
    if (len <= 1) { // 小于1就不需要进行比较了
        return arr
    }
    // 选取基准点
    var pointIndex = Math.floor(len / 2)
    // 截取基准点
    var point = arr.splice(pointIndex, 1)
    // 定义两个容器
    var left = []
    var right = []
    for(var i = 0; i < len - 1; i++) {
        if (arr[i] < point) {
            left.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }
    // 递归+拼接
    return [].concat(quickSort(left), point, quickSort(right))
}

手写 promise

思路:

  1. 可以改变执行器中的状态有哪些
    • 调用 resolve,返回成功的promise
    • 调用 reject,返回失败的promise
    • 抛出错误或程序异常,返回失败的promise
  2. then/catch:可以链式调用,所以要返回一个promise,该promise的状态和结果需要看对应回调函数的执行结果
    • 如果返回的是promise,则新返回的promise的状态跟随该promise
    • 如果是错误或抛出异常,则返回失败的promise
    • 其他情况,默认返回成功的promise
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function MyPromise(execute) {
  // 存储promise的结果
  this.result = undefined
  // 存储promise的状态
  this.stauts = PENDING
  // 存储的回调列表
  this.callbacks = []
  var _this = this
  // resolve 方法要做的事情:改变promise的状态变为成功,并保存结果
  function resolve(res) {
    _this.changeStatus(FULFILLED, res)
    if (_this.callbacks.length > 0) {
      _this.callbacks.forEach(function (item) {
        item[FULFILLED](_this.result)
      })
    }
  }
  // reject 方法要做的事情:改变promise的状态为失败,并保存结果
  function reject(res) {
    _this.changeStatus(REJECTED, res)
    if (_this.callbacks.length > 0) {
      _this.callbacks.forEach(function (item) {
        item[REJECTED](_this.result)
      })
    }
  }
  // 出现异常,改变promise状态
  try {
    execute(resolve, reject)
  } catch (error) {
    reject(error)
  }
}

// 改变promise状态的函数
MyPromise.prototype.changeStatus = function (status, res) {
  // 状态一旦改变了,就不能再变化了
  if (this.stauts !== PENDING) return
  this.result = res
  this.stauts = status
}

// 封装函数,处理回调
MyPromise.prototype.handleCallback = function (cb, onFulfilledCallback, onRejectedCallback) {
  try {
    // 获取执行的回调函数的结果,并判断这个结果是什么类型
    const result = cb(this.result)
    if (result instanceof MyPromise) {
      // 如果返回promise,则该promise跟随这个promise
      result.then(onFulfilledCallback, onRejectedCallback)
    } else {
      // 否则默认都是成功的promise
      onFulfilledCallback(result)
    }
  } catch (error) {
    // 如果是异常,则返回失败的promise
    onRejectedCallback(error)
  }
}

// 实现 then 方法
MyPromise.prototype.then = function (onFulfilled, onRejected) {
  // 处理值穿透的情况
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (res) { return res }
  onRejected = typeof onFulfilled === 'function' ? onRejected : function (res) { return res }
  // 可以链式调用,返回一个新的promise
  return new MyPromise((resolve, reject) => {
    if (this.stauts !== PENDING) {
      this.handleCallback(this.stauts === FULFILLED ? onFulfilled : onRejected, resolve, reject)
    } else {
      // 先指定了回调函数(延时触发execute中的resolve/reject),需要将回调函数保存起来 
      this.callbacks.push({
        [FULFILLED]: () => {
          this.handleCallback(onFulfilled, resolve, reject)
        },
        [REJECTED]: () => {
          this.handleCallback(onRejected, resolve, reject)
        }
      })
    }
  })
}

// 实现 catch 方法
MyPromise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected)
}

构造函数继承

function Parent(name) {
  this.name = name;
}
function Child(name) {
  Parent.call(this, name);
  this.type = "child 的属性";
}
console.log(new Child("zs"));
  • 优点:可以继承父类构造函数中的实例属性和方法
  • 缺点:不能继承父类原型中的属性和方法

原型链继承

function Person() {
  this.name = 'zs';
  this.age = 18
}

function Student() {
  this.score = 98;
}

// 方式一:这种方式会造成父类和子类的原型对象相互影响,指向的是同一个地址
Student.prototype = Person.prototype; // constructor 会指向 Object
// 方式二:这种方式会造成如果Person构造函数中返回的的并不是this,是其他的对象,那么对于Student来说则也不能访问Person原型中的属性和方法
Student.prototype = new Person(); // constructor 会指向 Person
// 方式三:推荐
Student.prototype = Object.create(Person.prototype); // constructor 会指向 Object

// 三种方式都需要设置 constructor 属性
Student.prototype.constructor = Student;

var s1 = new Student();
// 方式一的原型链为:s1.__proto__ => Student.prototype/Person.prototype => Object.prototype => null
// 方式二的原型链为: s1.__proto__ => Student.prototype/p1 => Person.prototype => Object.prototype => null
// 方式三的原型链为: s1.__proto__ => Student.prototype => Person.prototype => Object.prototype => null
  • 优点:可以继承父类原型上的属性和方法
  • 缺点:不能传递参数,不能使用父类构造函数中实例的属性和方法,并且每次创建的实例对象,都会创建一份新的原型数据,造成内存浪费
    • 如果是通过Student.prototype = new Person()的方式实现继承,那么会导致父类构造函数调用两次,以及父类的构造函数中的属性和方法会在子类的原型上显示,重复数据的问题

寄生组合继承

function Person(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
// 父类原型上添加的方法
Person.prototype.sayName = function () {
  console.log(this.name);
};

// 子类
function Student(name, grade) {
  Person.call(this, name); // 借用构造函数,实现对属性的继承
  this.grade = grade;
}
// 使用寄生方式继承父类原型
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student; // 将构造函数指回子类

Student.prototype.sayGrade = function () {
  console.log(this.grade);
};