JS 中 this 在各个场景下的指向(第二部分)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq449245884/article/details/99147734
 

作者:Dmitri Pavlutin
译者:前端小智
来源:dmitripavlutin.

原文的篇幅非常长,不过内容太过于吸引我,还是忍不住要翻译出来。我将原文分为上下两部分,本篇是第二部分,第一部分请看这:

JS 中 this 在各个场景下的指向

为了保证的可读性,本文采用意译而非直译。

 

 

5. 隐式调用

使用myFun.call()myFun.apply()方法调用函数时,执行的是隐式调用。

JS中的函数是第一类对象,这意味着函数就是对象,对象的类型为Function。从函数对象的方法列表中,.call().apply()用于调用具有可配置上下文的函数。

下面是隐式调用的例子

function increment(number) {  return ++number;  }increment.call(undefined, 10);    // => 11increment.apply(undefined, [10]); // => 11
  return ++number;  
}
increment.call(undefined10);    // => 11
increment.apply(undefined, [10]); // => 11

increment.call()increment.apply()都用参数10调用了这个自增函数。

两者的区别是.call()接受一组参数,例如myFunction.call(thisValue, 'value1', 'value2')。而.apply()接受的一组参数必须是一个类似数组的对象,例如myFunction.apply(thisValue, ['value1', 'value2'])。

5.1. 隐式调用中的this

在隐式调用.call()或.apply()中,this是第一个参数

很明显,在隐式调用中,this作为第一个参数传递给.call().apply()

var rabbit = { name: 'White Rabbit' };function concatName(string) {  console.log(this === rabbit); // => true  return string + this.name;}concatName.call(rabbit, 'Hello ');  // => 'Hello White Rabbit'concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'name'White Rabbit' };
function concatName(string{
  console.log(this === rabbit); // => true
  return string + this.name;
}
concatName.call(rabbit, 'Hello ');  // => 'Hello White Rabbit'
concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'

当应该使用特定上下文执行函数时,隐式调用非常有用。例如为了解决方法调用时,this总是window或严格模式下的undefined的上下文问题。隐式调用可以用于模拟在一个对象上调用某个方法。

function Runner(name) {  console.log(this instanceof Rabbit); // => true  this.name = name;  }function Rabbit(name, countLegs) {  console.log(this instanceof Rabbit); // => true  Runner.call(this, name);  this.countLegs = countLegs;}const myRabbit = new Rabbit('White Rabbit', 4);myRabbit; // { name: 'White Rabbit', countLegs: 4 }
  console.log(this instanceof Rabbit); // => true
  this.name = name;  
}
function Rabbit(name, countLegs{
  console.log(this instanceof Rabbit); // => true
  Runner.call(this, name);
  this.countLegs = countLegs;
}
const myRabbit = new Rabbit('White Rabbit'4);
myRabbit; // { name: 'White Rabbit', countLegs: 4 }

Rabbit中的Runner.call(this, name)隐式调用了父类的函数来初始化这个对象。

6. 绑定函数

绑定函数是与对象连接的函数。通常使用.bind()方法从原始函数创建。原始函数和绑定函数共享相同的代码和作用域,但执行时上下文不同。

方法 myFunc.bind(thisArg[, arg1[, arg2[, ...]]])接受第一个参数thisArg作为绑定函数执行时的上下文,并且它接受一组可选的参数 arg1, arg2, ...作为被调用函数的参数。它返回一个绑定了thisArg的新函数。

function multiply(number) {  'use strict';  return this * number;}const double = multiply.bind(2);double(3);  // => 6double(10); // => 20
  'use strict';
  return this * number;
}
const double = multiply.bind(2);

double(3);  // => 6
double(10); // => 20

bind(2)返回一个新的函数对象doubledouble 绑定了数字2multiplydouble具有相同的代码和作用域。

.apply().call() 方法相反,它不会立即调用该函数,.bind()方法只返回一个新函数,在之后被调用,只是this已经被提前设置好了。

6.1. 绑定函数中的this

在调用绑定函数时,this.bind()的第一个参数。

.bind()的作用是创建一个新函数,调用该函数时,将上下文作为传递给.bind()的第一个参数。它是一种强大的技术,使咱们可以创建一个定义了this值的函数。

640?wx_fmt=png

来看看,如何在如何在绑定函数设置 this

const numbers = {  array: [3, 5, 10],  getNumbers: function() {    return this.array;      }};const boundGetNumbers = numbers.getNumbers.bind(numbers);boundGetNumbers(); // => [3, 5, 10]// Extract method from objectconst simpleGetNumbers = numbers.getNumbers;simpleGetNumbers(); // => undefined (严格模式下报错)
  array: [3510],
  getNumbers: function() {
    return this.array;    
  }
};
const boundGetNumbers = numbers.getNumbers.bind(numbers);
boundGetNumbers(); // => [3, 5, 10]
// Extract method from object
const simpleGetNumbers = numbers.getNumbers;
simpleGetNumbers(); // => undefined (严格模式下报错)

numbers.getNumbers.bind(numbers)返回绑定numbers对象boundGetNumbers函数。boundGetNumbers()调用时的thisnumber对象,并能够返回正确的数组对象。

可以将函数numbers.getNumbers提取到变量simpleGetNumbers中而不进行绑定。在之后的函数调用中simpleGetNumbers()thiswindow(严格模式下为undefined),不是number对象。在这个情况下,simpleGetNumbers()不会正确返回数组。

6.2 紧密的上下文绑定

.bind()创建一个永久的上下文链接,并始终保持它。一个绑定函数不能通过.call()或者.apply()来改变它的上下文,甚至是再次绑定也不会有什么作用。

只有绑定函数的构造函数调用才能更改已经绑定的上下文,但是很不推荐的做法(构造函数调用必须使用常规的非绑定函数)。

下面示例创建一个绑定函数,然后尝试更改其已预先定义好的上下文

function getThis() {  'use strict';  return this;}const one = getThis.bind(1);// 绑定函数调用one(); // => 1// 使用带有.apply()和.call()的绑定函数one.call(2);  // => 1one.apply(2); // => 1// 再次绑定one.bind(2)(); // => 1// 以构造函数的形式调用绑定函数new one(); // => Object
  'use strict';
  return this;
}
const one = getThis.bind(1);
// 绑定函数调用
one(); // => 1
// 使用带有.apply()和.call()的绑定函数
one.call(2);  // => 1
one.apply(2); // => 1
// 再次绑定
one.bind(2)(); // => 1
// 以构造函数的形式调用绑定函数
new one(); // => Object

只有new one()改变了绑定函数的上下文,其他方式的调用中this总是等于1。

7. 箭头函数

箭头函数用于以更短的形式声明函数,并在词法上绑定上下文。它可以这样使用

const hello = (name) => {  return 'Hello ' + name;};hello('World'); // => 'Hello World'// Keep only even numbers[1, 2, 5, 6].filter(item => item % 2 === 0); // => [2, 6](name) => {
  return 'Hello ' + name;
};
hello('World'); // => 'Hello World'
// Keep only even numbers
[1256].filter(item => item % 2 === 0); // => [2, 6]

箭头函数语法简单,没有冗长的function 关键字。当箭头函数只有一条语句时,甚至可以省略return关键字。

箭头函数是匿名的,这意味着name属性是一个空字符串''。这样它就没有词法上函数名(函数名对于递归、分离事件处理程序非常有用)

同时,跟常规函数相反,它也不提供arguments对象。但是,这在ES6中通过rest parameters修复了:

const sumArguments = (...args) => {   console.log(typeof arguments); // => 'undefined'   return args.reduce((result, item) => result + item);};sumArguments.name      // => ''sumArguments(5, 5, 6); // => 16(...args) => {
   console.log(typeof arguments); // => 'undefined'
   return args.reduce((result, item) => result + item);
};
sumArguments.name      // => ''
sumArguments(556); // => 16

7.1. 箭头函数中的this

this 定义箭头函数的封闭上下文

箭头函数不会创建自己的执行上下文,而是从定义它的外部函数中获取 this。换句话说,箭头函数在词汇上绑定 this

640?wx_fmt=png

下面的例子说明了这个上下文透明的特性:

class Point {  constructor(x, y) {    this.x = x;    this.y = y;  }  log() {    console.log(this === myPoint); // => true    setTimeout(()=> {      console.log(this === myPoint);      // => true      console.log(this.x + ':' + this.y); // => '95:165'    }, 1000);  }}const myPoint = new Point(95, 165);myPoint.log();
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  log() {
    console.log(this === myPoint); // => true
    setTimeout(()=> {
      console.log(this === myPoint);      // => true
      console.log(this.x + ':' + this.y); // => '95:165'
    }, 1000);
  }
}
const myPoint = new Point(95165);
myPoint.log();

setTimeout使用与log()方法相同的上下文(myPoint对象)调用箭头函数。正如所见,箭头函数从定义它的函数继承上下文。

如果在这个例子里尝试用常规函数,它创建自己的上下文(window或严格模式下的undefined)。因此,要使相同的代码正确地使用函数表达式,需要手动绑定上下文:setTimeout(function(){…}.bind(this))。这很冗长,使用箭头函数是一种更简洁、更短的解决方案。

如果箭头函数在最顶层的作用域中定义(在任何函数之外),则上下文始终是全局对象(浏览器中的 window):

onst getContext = () => {   console.log(this === window); // => true   return this;};console.log(getContext() === window); // => true
   console.log(this === window); // => true
   return this;
};
console.log(getContext() === window); // => true

箭头函数一劳永逸地与词汇上下文绑定。即使修改上下文,this也不能被改变:

const numbers = [1, 2];(function() {    const get = () => {    console.log(this === numbers); // => true    return this;  };  console.log(this === numbers); // => true  get(); // => [1, 2]  // Use arrow function with .apply() and .call()  get.call([0]);  // => [1, 2]  get.apply([0]); // => [1, 2]  // Bind  get.bind([0])(); // => [1, 2]}).call(numbers);12];
(function() {  
  const get = () => {
    console.log(this === numbers); // => true
    return this;
  };
  console.log(this === numbers); // => true
  get(); // => [1, 2]
  // Use arrow function with .apply() and .call()
  get.call([0]);  // => [1, 2]
  get.apply([0]); // => [1, 2]
  // Bind
  get.bind([0])(); // => [1, 2]
}).call(numbers);

无论如何调用箭头函数get,它总是保留词汇上下文numbers。用其他上下文的隐式调用(通过 get.call([0])get.apply([0]))或者重新绑定(通过.bind())都不会起作用。

箭头函数不能用作构造函数。将它作为构造函数调用(new get())会抛出一个错误:TypeError: get is not a constructor

7.2. 陷阱: 用箭头函数定义方法

你可能希望使用箭头函数来声明一个对象上的方法。箭头函数的定义相比于函数表达式短得多:(param) => {...} instead of function(param) {..}

来看看例子,用箭头函数在Period类上定义了format()方法:

function Period (hours, minutes) {    this.hours = hours;  this.minutes = minutes;}Period.prototype.format = () => {  console.log(this === window); // => true  return this.hours + ' hours and ' + this.minutes + ' minutes';};const walkPeriod = new Period(2, 30);  walkPeriod.format(); // => 'undefined hours and undefined minutes'
  this.hours = hours;
  this.minutes = minutes;
}
Period.prototype.format = () => {
  console.log(this === window); // => true
  return this.hours + ' hours and ' + this.minutes + ' minutes';
};
const walkPeriod = new Period(230);  
walkPeriod.format(); // => 'undefined hours and undefined minutes'

由于format是一个箭头函数,并且在全局上下文(最顶层的作用域)中定义,因此 this 指向window对象。

即使format作为方法在一个对象上被调用如walkPeriod.format()window仍然是这次调用的上下文。之所以会这样是因为箭头函数有静态的上下文,并不会随着调用方式的改变而改变。

该方法返回'undefined hours和undefined minutes',这不是咱们想要的结果。

函数表达式解决了这个问题,因为常规函数确实能根据实际调用改变它的上下文:

function Period (hours, minutes) {    this.hours = hours;  this.minutes = minutes;}Period.prototype.format = function() {  console.log(this === walkPeriod); // => true  return this.hours + ' hours and ' + this.minutes + ' minutes';};const walkPeriod = new Period(2, 30);  walkPeriod.format(); // => '2 hours and 30 minutes'
  this.hours = hours;
  this.minutes = minutes;
}
Period.prototype.format = function() {
  console.log(this === walkPeriod); // => true
  return this.hours + ' hours and ' + this.minutes + ' minutes';
};
const walkPeriod = new Period(230);  
walkPeriod.format(); // => '2 hours and 30 minutes'

walkPeriod.format()是一个对象上的方法调用,它的上下文是walkPeriod对象。this.hours等于2this.minutes等于30,所以这个方法返回了正确的结果:'2 hours and 30 minutes'

总结

为函数调用对this影响最大,从现在开始不要问自己:

this 是从哪里来的?

而是要看看

函数是怎么被调用的?

对于箭头函数,需要想想

在这个箭头函数被定义的地方,this是什么?

这是处理this时的正确想法,它们可以让你免于头痛。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

交流

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

延伸阅读

JS中函数式编程基本原理简介

JS中轻松遍历对象属性的几种方式

什么时候不使用箭头函数

猜你喜欢

转载自blog.csdn.net/qq449245884/article/details/99147734