关于 this、bind、apply、call 的小记

217 阅读6分钟

this bind apply call

this bind apply call 拿了小姐姐的图用一用

this

在 javascript 中,this 的指向永远指向最后调用的那个对象。下面举三个例子简单说明这句话的意思(以下例子都是浏览器的运行结果)

例子一

function fn(obj) {
  console.log(this === obj)
}
fn(window) // true

例子一中 fn 中的打印的 this 结果很好理解,在全局作用域中调用 fn 函数,等同于 window.fn()。因此 fn 内部 this 的指向直到了调用 fn 的对象 window

例子二

let obj = {
  fn1(obj) {
    console.log(this === obj)
  },
  fn2(obj) {
    setTimeout(function callback() {
      console.log(this === obj)
    }, 0)
  }
}
obj.fn1(obj) // true
obj.fn2(window) // true

例子二通过字面量创建了一个对象 obj,obj 中有两个函数:fn1 和 fn2。fn1 的通过 obj 调用执行,因此 fn1 中的 this 指向 obj。而 fn2 中是通过 setTimeout 延迟了打印 this。setTimeout 会被推入事件队列中进行等待,直到主线程的调用栈中的所有代码执行结束后,再被推入调用栈中执行。这时 setTimeout 中的回调函数 callback 等同于通过 window 调用函数(window.callback())。与例子一相同,因此 callback 中的 this 指向 window

例子三

let obj1 = {
  fn(obj) {
    console.log(this === obj)
  }
}
let obj2 = {
  fn: obj1.fn,
  obj1
}
obj2.fn(obj2) // true
obj2.obj1.fn(obj1) // true

例子三中首先执行的是 obj2 中的 fn 方法。obj2 中的 fn 属性指向的是 obj1 中的 fn,但是最终是通过 obj2 来调用 fn 函数,因此此时函数中的 this 指向 obj2。而后面通过 obj2 访问了 obj1 并且调用了 fn 方法,最终调用 fn 对象的是 obj1,因此函数内的 this 指向 obj1

箭头函数

箭头函数是 es6 中的一个函数的扩展。具体可以看 阮一峰大神 es6 文档 中关于箭头函数的介绍。其中提到箭头函数的一个特性:箭头函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。 改造一下例子二来看一下这句话意思

let obj = {
  fn(obj) {
    setTimeout(() => {
      console.log(this === obj)
    }, 0)
  }
}
obj.fn(obj) // true

上面的代码中在 setTimeout 中的代码推入执行栈中执行时,回调函数是使用箭头函数定义的,在定义时上下文中的 this 就是 obj 对象。因此执行时函数中的 this 就是 obj 对象

bind

Function.prototype.bind 描述:创建返回一个新的函数,并且函数有指定的this值。
func.bind(thisArg[, argN])
thisArg 函数的 this 的指向的对象。如果使用 new 运算符构造绑定函数,则忽略该值。如果没有传递该值,这执行作用域的 this 则作为新函数的 thisArg
argN 置入新函数的参数列表中

bind 函数的应用

改变函数的 this 指向

let obj = {
  a: 1,
  b: 2
}
function sum() {
  return this.a + this.b
}

let objSum = sum.bind(obj)

objSum() // 3

缓存函数参数,创建拥有预设参数的函数

function sum(a, b) {
    return a + b
}
let plus = sum.bind(null, 1)
plus(9) // 10

apply

Function.prototype.apply
描述:方法调用一个具有指定 this 的函数,并返回函数的执行结果。
func.apply(thisArg[, argsArray])
thisArg func 执行时的 this。如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装
call方法的作用和 apply 方法类似,区别就是 call 方法接受的是参数列表,而 apply 方法接受的是一个参数数组。 argsArray 一个数组或者类数组对象。其中的元素将作为单独的参数依次传给 func 函数。

apply 函数的应用

用 apply 将数组添加到另一个数组

var array = ['a', 'b'];
var elements = [0, 1, 2];
array.push.apply(array, elements);
console.info(array); // ["a", "b", 0, 1, 2]

使用apply和内置函数

/* 找出数组中最大/小的数字 */
var numbers = [5, 6, 2, 3, 7];

/* 应用(apply) Math.min/Math.max 内置函数完成 */
var max = Math.max.apply(null, numbers); /* 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
var min = Math.min.apply(null, numbers);

/* 代码对比: 用简单循环完成 */
max = -Infinity, min = +Infinity;

for (var i = 0; i < numbers.length; i++) {
  if (numbers[i] > max)
    max = numbers[i];
  if (numbers[i] < min) 
    min = numbers[i];
}

上面的代码有个风险:当传入的参数过多的时候,就有可能导致越界的问题。而个数这个值是根据不同的 javascript 的引擎而定的。 当超过这个值时,有的引擎可能会抛出异常,有的引擎可能就会忽略超过后的参数,从而导致参数丢失。

可以使用以下的方法来解决

function minOfArray(arr) {
  var min = Infinity;
  var QUANTUM = 32768;

  for (var i = 0, len = arr.length; i < len; i += QUANTUM) {
    var submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)));
    min = Math.min(submin, min);
  }

  return min;
}
var min = minOfArray([5, 6, 2, 3, 7]);

call

Function.prototype.call
描述:方法调用一个指定 this 的函数和单独给出的一个或多个参数,并返回函数的执行结果
func.call(thisArg[, argN])
thisArg func 执行时的 this 值。如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
argN 一个或者多个参数。
该方法的语法和作用与 apply 方法类似,只有一个区别,就是 call 方法接受的是一个参数列表,而 apply 方法接受的是一个包含多个参数的数组。

call 函数的应用

使用 call 调用父构造函数

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}

var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);

使用 call 调用匿名函数

var animals = [
  { species: 'Lion', name: 'King' },
  { species: 'Whale', name: 'Fail' }
];

for (var i = 0; i < animals.length; i++) {
  (function(i) {
    this.print = function() {
      console.log('#' + i + ' ' + this.species
                  + ': ' + this.name);
    }
    this.print();
  }).call(animals[i], i);
}

使用 call 调用函数指定上下文的 this

function greet() {
  var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ');
  console.log(reply);
}

var obj = {
  animal: 'cats', sleepDuration: '12 and 16 hours'
};

greet.call(obj);  // cats typically sleep between 12 and 16 hours

手写 bind apply call

第一种实现方式:

Function.prototype.myCall = function (context, ...rest) {
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
  }
  context.fn = this
  let r = context.fn(...rest)
  delete context.fn
  return r
}

Function.prototype.myBind = function (context, ...rest) {
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
  }
  context.fn = this
  let r = function (...args) {
    return context.fn(...rest, ...args)
  }
  delete context.fn
  return r
}

Function.prototype.myApply = function (context, rest) {
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
  }
  context.fn = this
  let r = context.fn(...rest)
  delete context.fn
  return r
}

第二种实现方式

function makeFnProxy(context, propKey, fn) {
  return new Proxy(context, {
    get(target, propKey) {
      if (propKey === propKey) {
          return fn
      } else {
          return Reflect.get(target, propKey)
      }
    }
  })
}

Function.prototype.myCall = function (context, ...rest) {
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
  }
  const that = this
  const proxy = makeFnProxy(context, 'fn', that)
  const r = proxy.fn(...rest)
  return r
}

Function.prototype.myBind = function (context, ...rest) {
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
  }
  const that = this
  const proxy = makeFnProxy(context, 'fn', that)
  return function (...args) {
      return proxy.fn(...rest, ...args)
  }
}

Function.prototype.myApply = function (context, rest) {
  if (typeof this !== 'function') {
    throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
  }
  const that = this
  const proxy = makeFnProxy(context, 'fn', that)
  const r = proxy.fn(...rest)
  return r
}

这种方式的实现实际上绑定的 this 对象是 context 的一个代理,并不是原本的 context 对象

第三种实现方式

Function.prototype.myCall = function (context, ...rest) {
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    return this.apply(context, ...rest)
}

Function.prototype.myBind = function (context, ...rest) {
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    let that = this
    return function (...args) {
        return that.apply(context, ...rest, ...args)
    }
}

Function.prototype.myApply = function (context, rest) {
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    return this.call(context, ...rest)
}

参考文章

MDN 中 this 的介绍
箭头函数(阮一峰的 ES6 教程)
MDN 中 bind 的介绍
MDN 中 call 的介绍
MDN 中 apply 的介绍

有啥不对,欢迎指出