Javascript this全攻略

Javascript this

在JavaScript中,this 是当前执行函数的上下文。

JavaScript有4种不同的函数调用方式:

  • 函数调用: alert('Hello World!')
  • 方法调用: console.log('Hello World!')
  • 构造函数调用: new RegExp('\d')
  • 隐式调用: alert.call(undefined, 'Hello World!')

并且每种方法都定义了自己的上下文,this 会表现得跟程序员预期的不太一样。
同时,strict 模式也会影响函数执行时的上下文。

函数调用与方法调用

  • 函数调用:当一个表达式为函数接着一个 ( ,一些用逗号分隔的参数以及一个 ) 时,函数调用被执行。例如 parseInt('18')

  • 方法调用(属性访问):myObject.myFunction,是一个方法调用。[1,5].join(',') 是一个方法调用。实质就是对象属性的访问

在函数调用中的this

this 在函数调用中是一个全局对象

function sum(a, b) {
   console.log(this === window); // => true
   this.myNumber = 20; // add 'myNumber' property to global object
   return a + b;
}
// sum() is invoked as a function
// this in sum() is a global object (window)
sum(15, 16); // => 31
window.myNumber; // => 20

strict模式中, this 不再是全局对象而是变成undefined

function multiply(a, b) {
 'use strict'; // enable the strict mode
  console.log(this === undefined); // => true
  return a * b;
}
// multiply() function invocation with strict mode enabled
// this in multiply() is undefined
multiply(2, 5); // => 10

当multiply(2, 5)作为函数被调用时,this是undefined。

陷阱: 内部函数中的 this

一个函数调用中的常见错误就是以为this在内部函数中跟在外部函数中一样。

正确来说,内部函数的上下文依赖于调用方法,而不是外部函数的上下文。

var numbers = {
   numberA: 5,
   numberB: 10,
   sum: function() {
     console.log(this === numbers); // => true
     function calculate() {
       // this is window or undefined in strict mode
       console.log(this === numbers); // => false
       return this.numberA + this.numberB;
     }
     return calculate();
   }
};
numbers.sum(); // => NaN or throws TypeError in strict mode

numbers.sum() 是一个对象上的方法调用,所以 sum 中的上下文是 numbers 对象。

calculate 函数定义在 sum 内部,所以你会指望 calculate() 中的 this 也是 numbers 对象。

然而,calculate() 是一个函数调用(而不是方法调用),它的 this 是全局对象 window 或者 strict 模式下的 undefined

即使外部函数 sum 的上下文是 numbers 对象,它在这里也没有影响。

为了解决这个问题,calculate应该跟sum有一样的上下文,以便于使用numberA和numberB。解决方法之一是使用 .call() 方法

var numbers = {
   numberA: 5,
   numberB: 10,
   sum: function() {
     console.log(this === numbers); // => true
     function calculate() {
       console.log(this === numbers); // => true
       return this.numberA + this.numberB;
     }
     // use .call() method to modify the context
     return calculate.call(this);
   }
};
numbers.sum(); // => 15

方法调用

一个方法是作为一个对象的属性存储的函数。例如:

var myObject = {
  // helloFunction is a method
  helloFunction: function() {
    return 'Hello World!';
  }
};
var message = myObject.helloFunction();

方法调用中的 this

在方法调用中,this是拥有这个方法的对象

当调用一个对象上的方法时,this变成这个对象自身。

var calc = {
  num: 0,
  increment: function() {
    console.log(this === calc); // => true
    this.num += 1;
    return this.num;
  }
};
// method invocation. this is calc
calc.increment(); // => 1
calc.increment(); // => 2

构造函数调用

function Country(name, traveled) {
   this.name = name ? name : 'United Kingdom';
   this.traveled = Boolean(traveled); // transform to a boolean
}
Country.prototype.travel = function() {
  this.traveled = true;
};

var france = new Country('France', false);

france.traveled // false

france.travel();

france.traveled // true

从ECMAScript 6开始,JavaScript允许用class关键词来定义构造函数:

class City {
  constructor(name, traveled) {
    this.name = name;
    this.traveled = false;
  }
  travel() {
    this.traveled = true;
  }
}

var paris = new City('Paris', false);
paris.travel();

当属性访问 myObject.myFunction 前面有一个 new 关键词时,JavaScript会执行 构造函数调用 而不是原来的方法调用。

例如new myObject.myFunction():它相当于先用属性访问把方法提取出来extractedFunction = myObject.myFunction,

然后利用把它作为构造函数创建一个新的对象: new extractedFunction()。

构造函数中的 this

在构造函数调用中this指向新创建的对象

function Foo () {
  console.log(this instanceof Foo); // => true
  this.property = 'Default Value';
}
// Constructor invocation
var fooInstance = new Foo();
fooInstance.property; // => 'Default Value'

陷阱: 忘了new

function Vehicle(type, wheelsCount) {
  this.type = type;
  this.wheelsCount = wheelsCount;
  return this;
}
// Function invocation
var car = Vehicle('Car', 4);
car.type; // => 'Car'
car.wheelsCount // => 4
car === window // => true

你可能以为它正确地创建并初始化了对象。

然而,在函数调用中,thiswindow 对象,Vehicle('Car', 4) 实际上是在给 window 对象设置属性--这是错的。它并没有创建一个新的对象。

当你希望调用构造函数时,确保你使用了new操作符:

function Vehicle(type, wheelsCount) {
  if (!(this instanceof Vehicle)) {
    throw Error('Error: Incorrect invocation');
  }
  this.type = type;
  this.wheelsCount = wheelsCount;
  return this;
}
// Constructor invocation
var car = new Vehicle('Car', 4);
car.type // => 'Car'
car.wheelsCount // => 4
car instanceof Vehicle // => true

// Function invocation. Generates an error.
var brokenCat = Vehicle('Broken Car', 3);

在构造函数里我们添加了一个验证 this instanceof Vehicle 来确保执行的上下文是正确的对象类型。如果this不是Vehicle,那么就会报错。

隐式调用

当函数被.call()或者.apply()调用时,执行的是隐式调用。

方法.call(thisArg[, arg1[, arg2[, ...]]])将接受的第一个参数thisArg作为调用时的上下文,arg1, arg2, ...这些则 作为参数 传入被调用的函数。

方法.apply(thisArg, [args])将接受的第一个参数thisArg作为调用时的上下文,并且接受另一个 类似数组的对象[args] 作为被调用函数的参数传入。

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

隐式调用中的 this

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

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

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

绑定函数

绑定函数是一个与对象绑定的函数。通常它是通过在原函数上使用 .bind() 来创建的。原函数和绑定的函数共享代码跟作用域,但是在执行时有不同的上下文。

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

下面的代码创建了一个绑定函数并在之后调用它:

function multiply(number) {
 'use strict';
  return this * number;
}
// create a bound function with context
var double = multiply.bind(2);
// invoke the bound function
double(3); // => 6
double(10); // => 20
var say = concatName.bind(rabbit);
say('Hello'); // => 'Hello White Rabbit'

绑定函数中的 this

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

var numbers = {
  array: [3, 5, 10],
  getNumbers: function() {
    return this.array;
  }
};
// Create a bound function
var boundGetNumbers = numbers.getNumbers.bind(numbers);
boundGetNumbers(); // => [3, 5, 10]
// Extract method from object
var simpleGetNumbers = numbers.getNumbers;
simpleGetNumbers(); // => undefined or throws an error in strict mode

numbers.getNumbers函数能在不绑定的情况下赋值给变量simpleGetNumbers。

在之后的函数调用中,simpleGetNumbers()thiswindow 或者strict模式下的undefined,不是 number 对象。

在这个情况下,simpleGetNumbers()不会正确返回数组。

.bind() 永久性地建立了一个上下文的链接,并且会一直保持它。

一个绑定函数不能通过 .call() 或者 .apply() 来改变它的上下文,甚至是再次绑定也不会有什么作用。

只有用绑定函数的构造函数调用方法能够改变上下文,但并不推荐这个方法(因为构造函数调用用的是常规函数而不是绑定函数)。

下面的例子声明了一个绑定函数,接着试图改变它预先定义好的上下文:

function getThis() {
 'use strict';
  return this;
}
var one = getThis.bind(1);
// Bound function invocation
one(); // => 1
// Use bound function with .apply() and .call()
one.call(2); // => 1
one.apply(2); // => 1
// Bind again
one.bind(2)(); // => 1
// Call the bound function as a constructor
new one(); // => Object

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

箭头函数

箭头函数是 匿名的,这意味着它的name属性是个空字符串''。

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

箭头函数中的 this

this是箭头函数定义时封装好的上下文

箭头函数并不会创建它自己的上下文,它从它定义处的外部函数获得this上下文。下面的例子说明了这个上下文透明的特性:

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);
  }
}
var myPoint = new Point(95, 165);
myPoint.log();

setTimeout在调用箭头函数时跟 log() 使用了相同的上下文( myPoint对象 )。

正如所见,箭头函数从它定义处“继承”了函数的上下文。如果在这个例子里尝试用常规函数,它会建立自己的上下文( windowundefined )。

所以,为了让同样的代码能在函数表达式中正确运行,需要手动绑定上下文: setTimeout(function() {...}.bind(this) )。

这样一来就显得很啰嗦,不如用箭头函数来得简短。

如果箭头函数定义在最上层的作用域(在所有函数之外),那么上下文就总是全局对象(浏览器中的window对象):

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

箭头函数会一劳永逸地绑定词法作用域。即使使用修改上下文的方法,this也不能被改变:

var numbers = [1, 2];
(function() {
  var 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);

一个函数表达式通过.call(numbers)被隐式调用了,这使得这个调用的this变成了numbers。这样一来,箭头函数get的this也变成了numbers,因为它是从词法上获得的上下文。

无论get是怎么被调用的,它一直保持了一开始的上下文numbers。用其他上下文的隐式调用(通过.call()或.apply())或者重新绑定(通过.bind())都不会起作用

箭头函数不能用作构造函数。如果像构造函数一样调用new get(), JavaScript会抛出异常:TypeError: get is not a constructor。

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

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';
};
var walkPeriod = new Period(2, 30);
walkPeriod.format(); // => 'undefined hours and undefined minutes'

由于format是一个箭头函数,并且它定义在全局上下文(最顶层的作用域)中,它的this指向window对象。即使format作为方法在一个对象上被调用如walkPeriod.format(),window仍然是这次调用的上下文。之所以会这样是因为箭头函数有静态的上下文,并不会随着调用方式的改变而改变。

函数表达式可以解决这个问题,因为一个常规的函数会随着调用方法而改变其上下文:

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';
};
var walkPeriod = new Period(2, 30);
walkPeriod.format(); // => '2 hours and 30 minutes'

结论

因为函数调用对this有最大的影响,从现在起,不要再问你自己:

this是从哪里来的?

而要问自己:

函数是怎么被调用的?

对于箭头函数,问问你自己:

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

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

你可能感兴趣的:(Javascript this全攻略)