本文转载自:众成翻译 译者:MinweiShen 链接:http://www.zcfy.cc/article/901 原文:https://rainsoft.io/gentle-explanation-of-this-in-javascript/

1. this之谜

许多时候,this关键词对我以及许多刚起步的JavaScript程序员来说,都是一个谜。它是一种很强大的特性,但是理解它需要花不少功夫。

对有Java, PHP或者其他常见的编程语言背景的人来说,this仅仅被看成是类方法中当前对象的一个实例:不会多也不会少。多数时候,它不能在方法外被使用。正是这样一种简单的使用方法,避免了混淆。

在JavaScript中,this是当前执行函数的上下文。因为JavaScript有4种不同的函数调用方式:

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

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

理解this的关键点就是要对函数调用以及它如何影响上下文有个清晰的观点。这篇文章将会着重于对函数调用的解释、函数调用如何影响this以及展示确定上下文时常见的陷阱。

在开始之前,让我们来熟悉一些术语:

  • 函数调用 指执行构成一个函数的代码(简单说就是call一个函数)例如parseInt('15')parseInt函数调用.
  • 函数调用上下文this在函数体中的值。
  • 函数的作用域指的是在函数体内可以使用的变量、对象以及函数的集合。

2. 函数调用

当一个表达式为函数接着一个(,一些用逗号分隔的参数以及一个时,函数调用被执行。例如parseInt('18')。这个表达式不能是属性访问,如myObject.myFunction,因为这会变成一个方法调用。举个例子,[1,5].join(',')不是一个函数调用,而是一个方法调用。

一个简单的函数调用例子:

 function hello(name) {
return 'Hello ' + name + '!';
}
// Function invocation
var message = hello('World');
console.log(message); // => 'Hello World!'

hello('World')是函数调用: hello表达式等价于一个函数,跟在它后面的是一对括号以及'World'参数。

更加高级的例子是IIFE (立即调用的函数表达式):

 var message = (function(name) {
return 'Hello ' + name + '!';
})('World');
console.log(message) // => 'Hello World!'

IIFE也是一个函数调用: 第一对括号(function(name) {...}) 是一个等价于函数的表达式, 紧接着一对括号以及'World'参数: ('World')

2.1. 在函数调用中的this

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

全局对象是由执行的环境决定的。在浏览器里它是window对象。

在函数调用里,函数执行的上下文是全局对象。让我们一起看看下面函数里的上下文:

 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

sum(15, 16)被调用的时候,JavaScript自动设置this指向全局对象,也就是浏览器里的window

this在所有函数作用域以外(最上层的作用域:全局执行的上下文)调用时,它也指向全局对象:

 console.log(this === window); // => true
this.myString = 'Hello World!';
console.log(window.myString); // => 'Hello World!'

 <!-- In an html file -->  <script type="text/javascript"> console.log(this === window); // => true </script> 

2.2. 函数调用中的this, strict模式

strict模式下,函数调用中的thisundefined

strict模式在ECMAScript 5.1中被引入,它是一个受限制的JavaScript变种,提供了更好的安全性以及错误检查。为了使用它,把'use strict'放在函数体的开始。这个模式会影响执行的上下文,把this变成undefined。函数执行的上下文跟上面的例子2.1相反,不再是全局对象

在strict模式下执行函数的例子:

 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)作为函数被调用时,thisundefined

strict模式不仅在当前作用域起作用,也会对内部的作用域起作用(对所有在内部定义的函数有效):

 function execute() {
'use strict'; // activate the strict mode
function concat(str1, str2) {
// the strict mode is enabled too
console.log(this === undefined); // => true
return str1 + str2;
}
// concat() is invoked as a function in strict mode
// this in concat() is undefined
concat('Hello', ' World!'); // => "Hello World!"
}
execute();

'use strict' 插入在execute函数体的一开始, 使它在execute函数的作用域内起作用。 因为concat定义在execute的作用域内, 它也会继承strict模式, 这导致调用concat('Hello', ' World!')时, thisundefined

单个的JavaScript文件可能既包含strict模式又包含非strict模式。所以,在单个的脚本内,同样的调用方法可能有不同的上下文行为:

 function nonStrictSum(a, b) {
// non-strict mode
console.log(this === window); // => true
return a + b;
}
function strictSum(a, b) {
'use strict';
// strict mode is enabled
console.log(this === undefined); // => true
return a + b;
}
// nonStrictSum() is invoked as a function in non-strict mode
// this in nonStrictSum() is the window object
nonStrictSum(5, 6); // => 11
// strictSum() is invoked as a function in strict mode
// this in strictSum() is undefined
strictSum(8, 12); // => 20

2.3. 陷阱: 内部函数中的this

一个函数调用中的常见错误就是以为this在内部函数中跟在外部函数中一样。 正确来说,内部函数的上下文依赖于调用方法,而不是外部函数的上下文。 为了能使this跟预期的一样,用隐式调用来修改内部函数的上下文(用.call()或者.apply(), 如5.所示)或者创建一个绑定函数(用.bind(), 如6.所示)。

下面的例子计算了2个数字的和:

 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()是一个对象上的方法调用 (见3.),所以sum中的上下文是numbers对象。calculate函数定义在sum内部,所以你会指望calculate()中的this也是numbers对象。然而,calculate()是一个函数调用(而不是方法调用),它的this是全局对象window(例子2.1.)或者strict模式下的undefined(例子2.2.)。即使外部函数sum的上下文是numbers对象,它在这里也没有影响。numbers.sum()的调用结果是NaN或者strict模式下的TypeError: Cannot read property 'numberA' of undefined错误。因为calculate没有被正确调用,结果绝不是预期的5 + 10 = 15

为了解决这个问题,calculate应该跟sum有一样的上下文,以便于使用numberAnumberB。解决方法之一是使用.call()方法(见章节5.):

 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

calculate.call(this)像往常一样执行calculate,但是上下文由第一个参数指定。现在this.numberA + this.numberB相当于numbers.numberA + numbers.numberB,函数会返回预期的结果5 + 10 = 15

3. 方法调用

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

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

helloFunctionmyObject的一个方法。为了使用这个方法, 使用属性访问:myObject.helloFunction

当一个表达式以属性访问的形式执行时,执行的是方法调用,它相当于以个函数接着(,一组用逗号分隔的参数以及)。 利用前面的例子,myObject.helloFunction()是对象myObject上的一个helloFunction的方法调用。[1, 2].join(',')/\s/.test('beautiful world')也被认为是方法调用。

区分函数调用(见2.)跟方法调用是很重要的,因为他们完全不同。他们最主要的区别在于方法调用要求函数以属性访问的形式调用(如<expression>.functionProperty()或者<expression>['functionProperty']()),而函数调用并没有这样的要求(如<expression>())。

 ['Hello', 'World'].join(', '); // method invocation
({ ten: function() { return 10; } }).ten(); // method invocation
var obj = {};
obj.myFunction = function() {
return new Date().toString();
};
obj.myFunction(); // method invocation var otherFunction = obj.myFunction;
otherFunction(); // function invocation
parseFloat('16.60'); // function invocation
isNaN(0); // function invocation

3.1. 方法调用中的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

调用calc.increment()会把increment函数的上下文变成calc对象。所以,用this.num来增加num这个属性跟预期一样工作。

javaScript对象会从它的prototype继承方法。当这个继承的方法在新的对象上被调用时,上下文仍然是该对象本身:

 var myDog = Object.create({
sayName: function() {
console.log(this === myDog); // => true
return this.name;
}
});
myDog.name = 'Milo';
// method invocation. this is myDog
myDog.sayName(); // => 'Milo'

Object.create()创建了一个新的对象myDog,并且设置了它的prototype。myDog继承了sayName方法。当执行myDog.sayName()时,myDog是调用的上下文。

在ECMAScript 6的class语法中,方法调用的上下文也是这个实例本身:

 class Planet {
constructor(name) {
this.name = name;
}
getName() {
console.log(this === earth); // => true
return this.name;
}
}
var earth = new Planet('Earth');
// method invocation. the context is earth
earth.getName(); // => 'Earth'

3.2. 陷阱:从object中分离方法

一个对象中的方法可以赋值给另一个变量。当用这个变量调用方法时,你可能以为this指向定义这个方法的对象。

正确来说如果这个方法在没有对象的时候被调用,它会变成函数调用:this变成全局对象window或者strict模式下的undefined(见2.12.2)。 用绑定函数(用.bind(), 见6.)可以修正上下文,使它变成拥有这个方法的对象。

下面的例子创建了Animal构造函数并创造了它的一个实例 - myCatsetTimout()会在1秒钟之后输出myCat对象的信息:

 function Animal(type, legs) {
this.type = type;
this.legs = legs;
this.logInfo = function() {
console.log(this === myCat); // => false
console.log('The ' + this.type + ' has ' + this.legs + ' legs');
}
}
var myCat = new Animal('Cat', 4);
// logs "The undefined has undefined legs"
// or throws a TypeError in strict mode
setTimeout(myCat.logInfo, 1000);

你可能会以为setTimout会调用myCat.logInfo(),输出关于myCat对象的信息。实际上,这个方法在作为参数传递给setTimout(myCat.logInfo)时已经从原对象上分离了,1秒钟之后发生的是一个函数调用。当logInfo作为函数被调用时,this是全局对象,或者strict模式下的undefined(反正不是myCat对象),所以不会正确地输出信息。

函数可以通过.bind()方法跟一个对象绑定(见6.)。如果这个分离的方法与myCat绑定,那么上下文的问题就解决了:

 function Animal(type, legs) {
this.type = type;
this.legs = legs;
this.logInfo = function() {
console.log(this === myCat); // => true
console.log('The ' + this.type + ' has ' + this.legs + ' legs');
};
}
var myCat = new Animal('Cat', 4);
// logs "The Cat has 4 legs"
setTimeout(myCat.logInfo.bind(myCat), 1000);

myCat.logInfo.bind(myCat)返回一个跟logInfo执行效果一样的函数,但是它的this即使在函数调用情况下也是myCat

4. 构造函数调用

new关键词紧接着函数对象,(,一组逗号分隔的参数以及)时被调用,执行的是构造函数调用new RegExp('\\d')

这个例子声明了一个Country函数,并且将它作为一个构造函数调用:

 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;
};
// Constructor invocation
var france = new Country('France', false);
// Constructor invocation
var unitedKingdom = new Country; france.travel(); // Travel to France

new Country('France', false)Country函数的构造函数调用。它的执行结果是一个name属性为'France'的新的对象。 如果这个构造函数调用时不需要参数,那么括号可以省略:new Country

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

 class City {
constructor(name, traveled) {
this.name = name;
this.traveled = false;
}
travel() {
this.traveled = true;
}
}
// Constructor invocation
var paris = new City('Paris', false);
paris.travel();

new City('Paris')是构造函数调用。这个对象的初始化由这个类中一个特殊的方法constructor来处理。其中,this指向新创建的对象。

构造函数创建了一个新的空的对象,它从构造函数的原型继承了属性。构造函数的作用就是去初始化这个对象。 可能你已经知道了,在这种类型的调用中,上下文指向新创建的实例。这是我们下一章的主题。

当属性访问myObject.myFunction前面有一个new关键词时,JavaScript会执行构造函数调用不是原来的方法调用。例如new myObject.myFunction():它相当于先用属性访问把方法提取出来extractedFunction = myObject.myFunction,然后利用把它作为构造函数创建一个新的对象: new extractedFunction()

4.1. 构造函数中的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 Foo()正在调用一个构造函数,它的上下文是fooInstance。其中,Foo被初始化了:this.property被赋予了一个默认值。

同样的情况在用class语法(从ES6起)时也会发生,唯一的区别是初始化在constructor方法中进行:

 class Bar {
constructor() {
console.log(this instanceof Bar); // => true
this.property = 'Default Value';
}
}
// Constructor invocation
var barInstance = new Bar();
barInstance.property; // => 'Default Value'

new Bar()执行时,JavaScript创建了一个空的对象,把它作为constructor方法的上下文。现在,你可以用this关键词给它添加属性:this.property = 'Default Value'

4.2. 陷阱: 忘了new

有些JavaScirpt函数不是只在作为构造函数调用的时候才创建新的对象,作为函数调用时也会,例如RegExp

 var reg1 = new RegExp('\\w+');
var reg2 = RegExp('\\w+'); reg1 instanceof RegExp; // => true
reg2 instanceof RegExp; // => true
reg1.source === reg2.source; // => true

当执行new RegExp('\\w+')RegExp('\\w+')时,JavaScrit会创建相同的正则表达式对象。

因为有些构造函数在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

Vehicle是一个在上下文上设置了typewheelsCount属性的函数。当执行Vehicle('Car', 4)时,返回了一个car对象,它的属性是正确的:car.type'Car', car.wheelsCount4。你可能以为它正确地创建并初始化了对象。 然而,在函数调用中,thiswindow对象(见2.1.),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);

new Vehicle('Car', 4)工作正常:因为new关键词出现在构造函数调用前,一个新的对象被创建并初始化。 在构造函数里我们添加了一个验证this instanceof Vehicle来确保执行的上下文是正确的对象类型。如果this不是Vehicle,那么就会报错。这样,如果执行Vehicle('Broken Car', 3)(没有new),我们会得到一个异常:Error: Incorrect invocation

5. 隐式调用

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

函数在JavaScript中是第一类对象,这意味着函数也是对象。它的类型是Function。根据这个函数对象所拥有的方法列表.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

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;
}
// Indirect invocations
concatName.call(rabbit, 'Hello '); // => 'Hello White Rabbit'
concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'

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

另一个实际的例子是在ES5中,在创建的类的结构层次中中,调用父类的构造函数:

 function Runner(name) {
console.log(this instanceof Rabbit); // => true
this.name = name;
}
function Rabbit(name, countLegs) {
console.log(this instanceof Rabbit); // => true
// Indirect invocation. Call parent constructor.
Runner.call(this, name);
this.countLegs = countLegs;
}
var myRabbit = new Rabbit('White Rabbit', 4);
myRabbit; // { name: 'White Rabbit', countLegs: 4 }

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

6. 绑定函数

绑定函数是一个与对象绑定的函数。通常它是通过在原函数上使用 .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

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

.apply()以及.call()方法(见5.)马上调用函数不同,.bind()函数返回一个新的方法,它应该在之后被调用,只是this已经被提前设置好了。

6.1. 绑定函数中的this

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

.bind()的作用是创建一个新的函数,它在被调用时的上下文是传入.bind()的第一个参数。它是一种非常强大的技巧,使你可以创建一个定义了this值的函数。

让我们来看看如何在一个绑定函数中设置this

 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.bind(numbers)返回了一个绑定了number对象的boundGetNumbers函数。boundGetNumbers()调用时的thisnumber对象,并能够返回正确的数组对象。numbers.getNumbers函数能在不绑定的情况下赋值给变量simpleGetNumbers。在之后的函数调用中,simpleGetNumbers()thiswindow或者strict模式下的undefined,不是number对象(见3.2. 陷阱)。在这个情况下,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

7. 箭头函数

箭头函数被设计来以更简短的形式定义函数。并且能从词法上绑定上下文。它能以下面的方式被使用:

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

箭头函数带来了更轻量的语法,避免了冗长的function关键词。你甚至可以在函数只有一个语句的时候省略return

因为箭头函数是匿名的,这意味着它的name属性是个空字符串''。这样一来,它就没有一个词法上的函数名(函数名在递归跟事件解绑时会比较有用)。同时,跟常规函数相反,它也不提供arguments对象。但是,这在ES6中通过rest parameters修复了:

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

7.1. 箭头函数中的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对象)。正如所见,箭头函数从它定义处“继承”了函数的上下文。 如果在这个例子里尝试用常规函数,它会建立自己的上下文(window或strict模式下的undefined)。所以,为了让同样的代码能在函数表达式中正确运行,需要手动绑定上下文: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。这样一来,箭头函数getthis也变成了numbers,因为它是从词法上获得的上下文。

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

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

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

你可能想用箭头函数在一个对象上定义方法。这很合情合理:箭头函数的定义相比于函数表达式短得多:例如(param) => {...}而不是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';
};
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'

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

8. 结论

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

this是从哪里来的?

问自己:

函数是怎么被调用的?

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

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

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

JavaScript中‘this’关键词的优雅解释的更多相关文章

  1. javascript中window,document,body的解释

    解释javascript中window,document,body的区别: window对象表示浏览器中打开的窗口,即是一个浏览器窗口只有一个window对象. document对象是载入浏览器的ht ...

  2. Javascript中的原型继承具体解释

    js中的继承,是面向对象的知识,由于js没有类的概念.所以继承是通过对象实现的.谈到继承.就必须说到prototype,就不得不先说下new的过程. 一个小小的列子: <script type= ...

  3. 关于javascript中私有作用域的预解释

    1.如何区分私有变量还是全局变量 1).在全局作用域下声明(预解释的时候)的变量是全局变量 2).在“私有作用域中声明的变量”和“函数的形参”都是私有变量 在私有作用域中,我们代码执行的时候遇到一个变 ...

  4. JavaScript 中 关于 this 的学习笔记

    今天上午主要学习了js中的 this ,因为之前学习面向对象时,this这个东西出现的还是很频繁的,理解的很不透彻,感觉老被JAVA的思想带进坑里,所以对它特别关注. 首先贴一个大神的一篇博客,我是通 ...

  5. JavaScript中的内存泄漏以及如何处理

    随着现在的编程语言功能越来越成熟.复杂,内存管理也容易被大家忽略.本文将会讨论JavaScript中的内存泄漏以及如何处理,方便大家在使用JavaScript编码时,更好的应对内存泄漏带来的问题. 概 ...

  6. 【优雅代码】深入浅出 妙用Javascript中apply、call、bind

    这篇文章实在是很难下笔,因为网上相关文章不胜枚举. 巧合的是前些天看到阮老师的一篇文章的一句话: “对我来说,博客首先是一种知识管理工具,其次才是传播工具.我的技术文章,主要用来整理我还不懂的知识.我 ...

  7. JavaScript中的事件冒泡?事件传播的解释

    注:本文来源  可译网 事件冒泡是你在学习javaScript旅途中遇到的一个术语,它涉及到当一个元素被另一个元素嵌套时调用事件处理的顺序,并且两个元素注册了同一个事件(例如,点击事件). 但是事件冒 ...

  8. JavaScript中hoisting(悬置/置顶解析/预解析) 实例解释,全局对象,隐含的全局概念

    JavaScript中hoisting(悬置/置顶解析/预解析) 实例解释,全局对象,隐含的全局概念 <html> <body> <script type="t ...

  9. 关于javascript中静态成员和实例成员的详细解释

    关于javascript中静态成员和实例成员的详细解释  在我们了解什么是静态成员和实例成员之前,我们首先来了解一下什么是实例? 实例就是由构造函数创建出来的对象. 例如案例中 p 就是实例: fun ...

随机推荐

  1. Java企业实训 - 01 - Java前奏

    前言: 虽然个人专攻.NET方向,不过由于个人是干教育行业的,方方面面的东西,不能说都必须精通,但肯定多少都会涉及到. 一个菜鸟学员,从啥都不会,经过一步步学习,最后到企业上手掌管一个模块甚至一个项目 ...

  2. myrocks复制中断问题排查

    背景 mysql可以支持多种不同的存储引擎,innodb由于其高效的读写性能,并且支持事务特性,使得它成为mysql存储引擎的代名词,使用非常广泛.随着SSD逐渐普及,硬件存储成本越来越高,面向写优化 ...

  3. 大数据之Yarn——Capacity调度器概念以及配置

    试想一下,你现在所在的公司有一个hadoop的集群.但是A项目组经常做一些定时的BI报表,B项目组则经常使用一些软件做一些临时需求.那么他们肯定会遇到同时提交任务的场景,这个时候到底如何分配资源满足这 ...

  4. 一步步学习javascript基础篇(8):细说事件

    终于学到事件了,不知道为何听到“事件”就有一种莫名的兴奋.可能是之前的那些知识点过于枯燥无味吧,说起事件感觉顿时高大上了.今天我们就来好好分析下这个高大上的东西. 可以说,如果没有事件我们的页面就只能 ...

  5. 巧用location.hash保存页面状态

    在我们的项目中,有大量ajax查询表单+结果列表的页面,由于查询结果是ajax返回的,当用户点击列表的某一项进入详情页之后,再点击浏览器回退按钮返回ajax查询页面,这时大家都知道查询页面的表单和结果 ...

  6. ios 常见问题解决

    一,libxml/HTMLparser.h file not find 第一种方法: 点击左边项目的根目录,再点击右边的Build Settings,手工输入文字:“Header search pat ...

  7. Atitit 软件架构方法的进化与演进cs bs soa roa  msa  attilax总结

    Atitit 软件架构方法的进化与演进cs bs soa roa  msa  attilax总结 1.1. 软件体系架构是沿着单机到 CS 架构,再到 BS 的三层架构甚至多层架构逐步发展过来的,关于 ...

  8. 【Win 10 应用开发】手写识别

    记得前面(忘了是哪天写的,反正是前些天,请用力点击这里观看)老周讲了一个14393新增的控件,可以很轻松地结合InkCanvas来完成涂鸦.其实,InkCanvas除了涂鸦外,另一个大用途是墨迹识别, ...

  9. 8.SVM用于多分类

    从前面SVM学习中可以看出来,SVM是一种典型的两类分类器.而现实中要解决的问题,往往是多类的问题.如何由两类分类器得到多类分类器,就是一个值得研究的问题. 以文本分类为例,现成的方法有很多,其中一劳 ...

  10. Vue1.0 的技术栈

    vuejs概述 Vue.js是用于构建交互式的Web界面的库.它提供了MVVM数据绑定和一个可组合的组件系统,具有简单.灵活的API. 结合node.js 可以实现前后端开发从物理上的分离.使前端负责 ...