JS中的this指向,call,apply,bind 的实现

39 阅读4分钟

JavaScript 中函数的调用方式决定了 this 的值(运行时绑定)

各种情形下的 this 指向

1,在构造函数中,this 就是实例对象

let _this = null
function Parent(name, age) {
  this.name = name
  this.age = age
  _this = this
}
const p = new Parent('zhangsan', 18)
console.log(p === _this) // true;

2,以方法调用的函数,this 就是调用的对象

let _this = null
const obj = {
  name: 'zhangsan',
  sayName() {
    console.log(this.name)
    _this = this
  }
}
obj.sayName() // zhangsan
console.log(obj === _this) // true;

3,在 DOM事件中,this 就是触发事件的元素

<body>
  <button id="btn">btn</button>
</body>
<script>
  const btn = document.getElementById('btn')
  btn.addEventListener('click', function() {
    console.log(this === btn) // true
  })
</script>

4,作为函数直接调用时,this 指向全局对象

<script>
  function consoleThis(){
    console.log('this', this);
  }
  consoleThis(); // Window
</script>
function consoleThis(){
  console.log('this', this);
}
consoleThis() // global

5,在浏览器环境下,不通过函数,直接使用 this,this 指向全局变量

<script>
  console.log(this); // window
</script>

6,在 nodejs 的模块文件中,直接使用 this,this 指向 默认的 module.exports

console.log(this === module.exports);
const modulea = require('./a.js')

运行 node b.js会输出 true

7,在箭头函数中使用 this,this 的取值就是在箭头函数被创建时,当前上下文中的 this,和调用方式无关

下面代码中,外部arrFn 在创建时,它外部的 this 是 module.exports,所以无论通过哪种调用方式, this 都是module.exports

而 Foo 中的 arrFn 在创建时,它外部的 this,是函数 Foo中的 this,所以它打印出的 this,一直和 Foo 中直接打印的 this 是一致的

const arrFn = () => this
const callArr = function() {
  return arrFn()
}
const obj = {
  name: 'zhangsan',
  callArr,
  arrFn,
  objArr: () => this
}
console.log(arrFn() === module.exports) // true;
console.log(obj.arrFn() === module.exports) // true;
console.log(obj.callArr() === module.exports) // true;
console.log(obj.objArr() === module.exports) // true;

function Foo() {
  console.log('Foo this', this);
  const arrFn = () => {console.log(this);}
  arrFn()
}
Foo() // 打印 2 次 global
new Foo() // 打印 2 次 Foo
obj.Foo = Foo
obj.Foo() // 打印 2 次 obj

const obj2 = {
  name: 'obj2'
}
Foo.bind(obj2)() // 打印 2 次 obj2
Foo.call(obj2) // 打印 2 次 obj2
Foo.apply(obj2) // 打印 2 次 obj2

8,通过 call,apply,bind 可以修改 this 的指向,传入的第一个参数就是 this 的指向

const obj = {
  name: 'zhangsan'
}
function consoleThis(){
  console.log('this', this);
}
consoleThis() // global
consoleThis.call(obj) // obj
consoleThis.apply(obj) // obj
consoleThis.bind(obj)() // obj

实现 call,apply

实现方式:

  1. 将该函数(也就是原有的 this)绑定为传入的this对象的属性
  2. 通过传入的 this对象调用该函数

注意点:

  1. 需要判断调用者必须是个函数
  2. 需要判断传入的 context 如果为 null 或 undefined,改为全局对象
  3. 在绑定方法的时候可能会覆盖context原有的方法,所以属性名最好是用 Symbol
  4. 调用结束后要删除掉绑定的属性,避免污染传入的 context
  5. call 和 apply要 return 函数调用的返回值
Function.prototype.myCall = function(context, ...args) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  if (context === null || context === undefined) {
    context = window || global
  } else {
    context = Object(context)
  }
  var fnName = Symbol()
  context[fnName] = this
  const result = context[fnName](...args)
  delete context[fnName]
  return result
}

Function.prototype.myApply = function(context, args = []) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  if (context === null || context === undefined) {
    context = window || global
  } else {
    context = Object(context)
  }
  var fnName = Symbol()
  context[fnName] = this
  const result = context[fnName](...args)
  delete context[fnName]
  return result
}

实现 bind

在实现 bind 时,除了上面的一些判断,还需要注意的一些细节

1,bind 在绑定的时候,是可以进行科里化传参的,保留前置传入的参数

function foo(name, age, addr) {
  console.log(this.value, name, age, addr);
}
const obj = {
  value: 'testbind'
}
const b0 = foo.bind(obj)
b0() // testbind undefined undefined undefined
const b1 = foo.bind(obj, 'zhangsan')
b1() // testbind zhangsan undefined undefined
const b2 = b1.bind(obj, 12)
b2() // testbind zhangsan 12 undefined
b2('beijing') // testbind zhangsan 12 beijing

2,返回的函数,如果作为构造函数使用,提供的 this 会被忽略,但前置的参数仍然会保留

function foo(name, age, addr) {
  console.log(this.value, name, age, addr);
}
const obj = {
  value: 'testbind'
}
const b1 = foo.bind(obj, 'zhangsan')
b1() // testbind zhangsan undefined undefined
new b1() // undefined zhangsan undefined undefined

3,实现思路

  1. 对于参数保留,将本次传入的...args和返回值函数调用的arguments组装,最后使用 apply 调用
  2. 对于返回函数的处理,如果是通过 new 调用,当前的this 就应该是实例对象,也就是F 函数中的 this,否则是传入的 context
  3. 由于返回的函数时可以 new 创建实例的,所以要修改 F 的原型,指向最初调用时函数的原型,否则原型链就被破坏了。修改后再使用 new 调用返回的函数,原型和直接 new foo 是一致的

4,具体代码和测试

Function.prototype.myBind = function(context, ...args) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  const _this = this
  function F() {
    const bindArgs = Array.prototype.slice.call(arguments)
    return _this.apply(this instanceof F ? this : context, args.concat(bindArgs))
  }
  F.prototype = Object.create(this.prototype)
  return F
}

// 测试代码
function foo(name, age, addr) {
  console.log(this.value, name, age, addr);
}
foo.prototype.prototypeBindProperty = 'prototype bind property'
const obj = {
  value: 'testbind'
}
const b0 = foo.myBind(obj)
b0() // testbind undefined undefined undefined
const b1 = foo.myBind(obj, 'zhangsan')
b1() // testbind zhangsan undefined undefined
const res = new b1() // undefined zhangsan undefined undefined
console.log(res.prototypeBindProperty); // prototype bind property
const b2 = b1.myBind(obj, 12)
b2() // testbind zhangsan 12 undefined
b2('beijing') //  testbind zhangsan 12 beijing