JavaScript如何实现new、apply、call、bind 的底层逻辑

208 阅读3分钟

继承进阶:如何实现 new、apply、call、bind 的底层逻辑

一、new 原理介绍

new 关键词的主要作用就是执行一个构造函数、返回一个实例对象,在 new 的过程中,根据构造函数的情况,来确定是否可以接受参数的传递。

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(this指向新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象
  • new 关键词执行之后总是会返回一个对象,要么是实例对象,要么是 return 语句指定的对象
function Person() {
  this.name = 'person'
}
let p = new Person()
console.log(p.name)
let p1 = Person()
console.log(p1)
console.log(name)
// JS代码在默认情况下this的指向是window

function Person1() {
  this.name = 'person1'
  return { name: 'return' }
}
let p2 = new Person1()
console.log(p2)
console.log(p2.name)

function Person2() {
  this.name = 'person2'
  return 'return'
}
let p3 = new Person2()
console.log(p3)
console.log(p3.name)

二、apply & call & bind 原理介绍

call、apply和bind是挂在Function对象上的三个方法,调用这三个方法的必须是一个函数

func.call(thisArg,param1,param2,...)
func.apply(thisArg,[param1,param2,...])
func.bind(thisArg,param1,param2,...)
  • 其中 func 是要调用的函数,thisArg一般为this所指向的对象,后面的param1、2为函数 func 的多个参数,如果 func 不需要参数,则后面的 param1、2可以不写

  • 这三个方法共有的、比较明显的作用就是,都可以改变函数func的this指向。call和apply的区别在于,传参的写法不同:apply的第2个参数为数组;call则是从第2个至第N个都是给func的传参;而bind和这两个(call、apply)又不同,bind虽然改变了func的this指向,但不是马上执行,而这两个(call、apply)是在改变了函数的this指向之后立马执行。

let a = {
  name: 'jack',
  getName: function (msg) {
    return msg + this.name
  },
}
let b = {
  name: 'lily',
}
console.log(a.getName('hello~'))
console.log(a.getName.call(b, 'hi~'))
console.log(a.getName.apply(b, ['hi~']))
let name = a.getName.bind(b, 'hello~')
console.log(name())

三、方法的应用场景

1.判断数据类型

// 1. 判断数据类型
function getType(obj) {
  let type = typeof obj
  if (type !== 'object') {
    return type
  }
  return Object.prototype.toString.call(obj).replace(/^$/, '$1')
}

2.类数组借用方法

// 2. 类数组借用方法
const arrayLike = {
  0: 'java',
  1: 'script',
  length: 2,
}
Array.prototype.push.call(arrayLike, 'jack', 'lily')
console.log(typeof arrayLike)
console.log(arrayLike)

3.获取数组的最大/最小值

// 3. 获取数组的最大/最小值
let arr = [13, 6, 10, 11, 16]
const max = Math.max.apply(Math, arr)
const min = Math.min.apply(Math, arr)
console.log(max)
console.log(min)
// 减少一步展开数组
console.log(max === Math.max(...arr))

4.继承

// 4. 继承
function Parent() {
  this.name = 'parent'
  this.arr = [1, 2, 3]
}
Parent.prototype.getName = function () {
  return this.name
}
function Child() {
  Parent.call(this)
  this.type = 'child'
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
let child = new Child()
console.log(child.getName())

四、如何自己实现这些方法?

1.new 的实现

  1. 让实例可以访问到私有属性
  2. 让实例可以访问构造函数原型所在原型链上的属性
  3. 构造函数返回的最后结果是引用数据类型
function _new(ctor, ...args) {
  if (typeof ctor !== 'function') {
    throw 'ctor must be a function'
  }
  let obj = new Object()
  obj.__proto__ = Object.create(ctor.prototype)
  let res = ctor.apply(obj, [...args])
}

2.apply和call的实现

Function.prototype.call = function (context, ...args) {
  var context = context || window
  context.fn = this
  var result = eval('context.fn(...args)')
  delete context.fn
  return result
}
Function.prototype.apply = function (context, args) {
  let context = context || window
  context.fn = this
  let result = eval('context.fn(...args)')
  delete context.fn
  return result
}

3.bind 的实现

bind 的实现思路基本和apply一样,但是在最后实现返回结果这里,bind和apply有着比较大的差异,bind不需要直接执行,因此不在需要eval,而是需要通过返回一个函数的方式将结果返回,之后再通过执行这个结果,得到想要的执行效果

Function.prototype.bind = function (context, ...args) {
  if (typeof this !== 'function') {
    throw new Error('this must be a function')
  }
  var self = this
  var fbound = function () {
    self.apply(
      this instanceof self ? this : context,
      args.concat(Array.prototype.slice.call(arguments))
    )
  }
  if (this.prototype) {
    fbound.prototype = Object.create(this.prototype)
  }
  return fbound
}

方法/特征callapplybind
方法参数多个单个数组多个
方法功能函数调用改变 this函数调用改变 this函数调用改变 this
返回结果直接执行的直接执行返回待执行函数
底层实现通过 eval通过 eval间接调用 apply