JS中this的各个环境下指向(一)

8月14日学习笔记

JS中this在各个环境下指向

函数调用中的this

this在函数调用中为全局对象

全局对象由执行环境决定。在浏览器中,this是window对象
在函数调用中,执行上下文是全局对象:

1
2
3
4
5
6
function sum(a,b){
this.myNum=50;
return a+b;
}
var c=sum(3,4);
console.log(c,window.myNum);

在浏览器中运行输出为

7 50

可以看见在调用sum函数时,this设置为全局对象,指向的对象是window
同样的

当this在任意函数作用域之外使用,this表示window对象

1
2
3
4
console.log(this===window);//=>true
this.myString='hello,this';
console.log(window.myString);//=>hello,this
console.log(this===window);//true

此外,在严格模式下,由于严格模式会影响函数上下文,因此this在常规函数中值为undefined,执行上下文不再是全局对象

1
2
3
4
5
6
7
function sum(a,b){
'use strict'
this.myNum=50;
return a+b;
}
var c=sum(3,4);
console.log(window.myNum);

此时会触发一个erro:

Uncaught TypeError: Cannot set property ‘myNum’ of undefined

陷阱:this 在内部函数中的时候

函数调用一个常见陷阱是,认为this在内部函数中情况与外部函数中情况相同。
正确来说,内部函数上下文只依赖与它的调用类型而不依赖于外部函数的上下文。
要将this设置为所需的值,可以通过.call或.apply()修改内部函数的上下文或使用.bind()创建绑定函数。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
const numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // => true
function calculate() {
console.log(this===window); // => true
return this.numberA + this.numberB;
}
return calculate();
}
};
numbers.sum(); // => NaN

在这里,sum()是对象上的方法调用,上下文是numbers对象,calculate函数是sum中定义的,是一个函数调用(不是方法调用),在非严格模式下将this作为全局对象window。即使外部函数将上下文作为numbers对象,但在calculate里面没有影响,因此sum()调用结果为NaN,原因是没有正确调用calculate。
为了解决这个问题,calculate函数中上下文应与sum中一样来访问numbers的属性numberA和numberB
一种方式是通过调用calculate.call(this)手动将calculate上下文更改为所需的上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // => true
function calculate() {
console.log(this === numbers); // => true
return this.numberA + this.numberB;
}
// 使用 .call() 方法修改上下文
return calculate.call(this);
}
};
numbers.sum(); // => 15

call(this)将上下文修改为指定的第一个参数的值来执行calculate
另外一种是使用箭头函数

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的thisargumentssupernew.target。这些函数表达式更适用于那些本来需要匿名函数的地方,并且它们不能用作构造函数。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
const numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // => true
const calculate = () => {
console.log(this === numbers); // => true
return this.numberA + this.numberB;
}
return calculate();
}
};
numbers.sum(); // => 15

方法调用

方法是存储在对象属性中的函数
例如:

1
2
3
4
5
6
7
8
9
10
const numbers = {
numberA: 5,
numberB: 10,

hello: function() {
return 'hello!'
}
};
const hi=numbers.hello();
console.log(hi);//=>hello!

hello()是numbers的一个方法,通过numbers.hello()调用.

当一个表达式以属性访问的形式执行时,执行的执行的是方法调用,它相当于以个函数接着(一组用逗号分隔的参数)。

numbers.hello()是对象上一个hello的方法调用.
函数调用与方法调用是不同的类型,区别在于方法调用需要属性访问器形式来调用函数,而函数调用不需要.

1
2
3
4
5
6
7
8
9
10
11
12
['Hello', 'World'].join(', '); // 方法调用
({ ten: function() { return 10; } }).ten(); // 方法调用
const obj = {};
obj.myFunction = function() {
return new Date().toString();
};
obj.myFunction(); // 方法调用

const otherFunction = obj.myFunction;
otherFunction(); // 函数调用
parseFloat('16.60'); // 函数调用
isNaN(0); // 函数调用

方法调用中的this

当调用对象上的方法时,this就成了对象本身
例如:

1
2
3
4
5
6
7
8
9
10
11
const numbers = {
numberA: 5,
numberB: 10,
sum:function(){
console.log(this===numbers)
this.numberA += 1;
return this.numberA;
}
};
console.log( numbers.sum()); //true 6
console.log( numbers.sum());//true 7

这段代码说明:调用numbers.sum()使得sum函数的上下文成为numbers的对象,所以使用this可以访问numbers对象的属性

JS对象从原型继承一个方法,当在对象上调用继承的方法时,调用的上下文仍然是对象本身

1
2
3
4
5
6
7
8
9
10
11
const numbers = Object.create({
numberA: 5,
numberB: 10,
sum:function(){
console.log(this===numbers)//=>true
this.numberX += 1;
return this.numberX;
}
});
numbers.numberX=9;
console.log(numbers.sum())//=>10

这里通过Object.create创建了新对象numbers,根据第一个参数设置原型。numbers对象继承sum方法。
执行numbers.sum()时,numbers是调用的上下文。

在ES6 class 语法中,方法调用上下文也是实例本身

1
2
3
4
5
6
7
8
9
10
11
class Planet{
constructor(name){
this.name=name;
}
getName(){
console.log(this===earth);
return this.name;
}
}
var earth=new Planet('Earth');
console.log(earth.getName()); //=> true Earth

陷阱:将方法与其对象分离

方法可以从对象中提取到一个单独的变量 const alone=myObj.myMethod当方法单独掉用时,与原始对象alone分离,你可能认为当前的this对象就是定义方法的对象myObj
如果方法在没有对象的情况下调用,那么实际发生的是函数调用,此时this指向的是全局对象window,在严格模式下为undefined。
例如

1
2
3
4
5
6
7
8
9
10
function pets(type,legs){
this.type=type;
this.legs=legs;
this.Info=function(){
console.log(this===myCat);//=>false
console.log('The'+this.type+'has'+this.legs+'legs');//The undefined has undefined legs
}
}
const myCat=new pets('cat',4);
setTimeout(myCat.Info,1000);

你可能认为setTimout调用myCat.Info()时,它应该打印关于myCat对象的信息。
不幸的是,方法在作为参数传递时与对象是分离,setTimout(myCat.Info)以下情况是等效的:

1
2
3
4
setTimeout(myCat.Info);
//等价于
const extractedInfo=myCat.Info;
setTimeout(extractedInfo);

将分离的Info作为函数调用时,this是全局window,所以对象信息没有正确的打印。
函数可以使用.bind()方法与对象绑定,解决this指向的问题。

1
2
3
4
5
6
7
8
9
10
function pets(type,legs){
this.type=type;
this.legs=legs;
this.Info=function(){
console.log(this===myCat);//=>true
console.log('The'+this.type+'has'+this.legs+'legs');//The undefined has undefined legs
}
}
const myCat=new pets('cat',4);
setTimeout(myCat.Info.bind(myCat),1000);//=>thecathas4legs

myCat.Info.bind(myCat)返回一个新函数,它的执行方法与Info完全相同,单身此时的this 指向myCat ,即使在函数调用中也是如此。
另一种解决方式是将Info()方法定义为一个箭头函数:

1
2
3
4
5
6
7
8
9
10
function pets(type,legs){
this.type=type;
this.legs=legs;
this.Info=>(){
console.log(this===myCat);//=>true
console.log('The'+this.type+'has'+this.legs+'legs');//The undefined has undefined legs
}
}
const myCat=new pets('cat',4);
setTimeout(myCat.Info.bind(myCat),1000);//=>thecathas4legs

构造函数调用

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

1
2
3
4
5
6
7
8
9
10
11
12
13
function Country(name, traveled) {
this.name = name ? name : 'United Kingdom';
this.traveled = Boolean(traveled);
}
Country.prototype.travel = function() {
this.traveled = true;
};
// 构造函数调用
const france = new Country('France', false);
// 构造函数调用
const unitedKingdom = new Country;

france.travel(); // Travel to France

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

从ES6开始,JS 允许用class关键词来定义构造函数

1
2
3
4
5
6
7
8
9
10
11
12
class City {
constructor(name, traveled) {
this.name = name;
this.traveled = false;
}
travel() {
this.traveled = true;
}
}
// Constructor invocation
const paris = new City('Paris', false);
paris.travel();

new City('Paris')是构造函数调用。这个对象的初始化由这个类中一个特殊的方法constructor来处理。其中,this指向新创建的对象。
构造函数创建了一个新的空的对象,它从构造函数的原型继承了属性。构造函数的作用就是去初始化这个对象。 可能你已经知道了,在这种类型的调用中,上下文指向新创建的实例。(谁调用,指向谁)
当属性访问myObject.myFunction前面有一个new关键词时,JS会执行构造函数调用而不是原来的方法调用。
例如new myObject.myFunction():它相当于先用属性访问把方法提取出来extractedFunction = myObject.myFunction,然后利用把它作为构造函数创建一个新的对象: new extractedFunction()

构造函数中的this

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

构造函数调用的上下文是新创建的对象。它利用构造函数的参数初始化新的对象,设定属性的初始值,添加事件处理函数等。
20190815201609.png
可以看见这里的this是指向通过构造函数新创建的对象
例如在以下代码中:

1
2
3
4
5
6
7
class Foo{
constructor(){
this.property='Default Value';
}
}
const fooInstance=new Foo();
console.log(fooInstance.property);//=>Default Value

new Foo() 调用构造函数,上下文是新创建的对象fooInstance 。在constructor方法内部初始化对象:this.property 赋值为默认值。

陷阱:忘记使用new

某些JS函数不是只在作为构造函数的时候创建新对象,在作为函数调用的时候也会创建新对象。
例如RegExp:

RegExp 对象表示正则表达式,它是对字符串执行模式匹配的强大工具。

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

使用函数调用来创建对象存在一个潜在的问题(这里不包括工厂模式),即:

一些构造函数可能会忽略在缺少new 关键词时初始对象的逻辑

例如:

1
2
3
4
5
6
7
8
9
10
function Pets(type,legs){
this.type=type;
this.legs=legs;
return this;
}
//未使用new
const cat=Pets('cat',4);
console.log(cat.type); //=> cat
console.log(cat.legs); // => 4
console.log(cat===window); // =>true

这里的this没有指向cat而是指向了全局对象window,因为在函数调用中,thiswindow 对象,Pets('cat',4)window 对象上设置属性。显然这属于错误,它并没有创建新对象。

因此在创建新对象的时候,要确保使用了new 操作符

隐式调用

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

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

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

下面是隐式调用的例子

1
2
3
4
5
6
7
function sum(number){
number+=1;
return number;
}

console.log(sum.call(undefined,11)); //=> 12
console.log(sum.apply(undefined,[11]));//=> 12

这两个方法都使用参数11调用sum函数
两者的区别是,.call() 接受一组参数,myFunction.call(thisValue, ‘value1’, ‘value2’)而.apply() 接受的一组参数必须是一个类似数组的对象,例如myFunction.apply(thisValue, ['value1', 'value2'])。

隐式调用中的this

在隐式调用.call()或.apply()中,this为传递的第一个参数

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

1
2
3
4
5
6
7
8
9
10
var cat ={
name : 'xiaobai'
}
function concatName(string){
console.log(this === cat); //=>true
return string+this.name;
}
const a= concatName.call(cat,'hello');//=>helloxiaobai
const b=concatName.apply(cat,['goodbye']);//=>goodbyexiaobai
console.log(a+' '+b);

当应该使用特定上下文执行函数时,隐式调用非常有用。例如为了解决方法调用时,this 总是指向window 或严格模式下undefined 的上下文问题。

隐式调用可以用于模拟在一个对象上调用某个方法。

1
2
3
4
5
6
7
8
9
10
11
12
function Runner(name){
console.log(this instanceof Cat); //=>true
this.name=name;
}

function Cat(name,legs){
console.log(this instanceof Cat); //=>true
Runner.call(this,name);
this.legs=legs;
}
const myCat=new Cat('xiaobai',4);
console.log(myCat); //=>Cat {name: "xiaobai", legs: 4}

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