javascript之this详解(下)

2016-07-01

4. 构造函数调用

构造函数调用使用 new 关键词,后面跟随可带参数的对象表达式,例:new RegExp('\\d')

以下的例子声明了一个构造函数 Country,并调用。

function Country(name, traveled) {  
  this.name = name ? this.name : 'United Kingdom';
  this.traveled = Boolean(traveled); // 转换为 boolean 值
}
Country.prototype.travel = function() {  
  this.traveled = true;
};
// 构造函数调用
var france = new Country('France', false);  
// 构造函数调用
var unitedKingdom = new Country;

france.travel(); // Travel to France 

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

构造器调用创建了一个空的新对象,从构造器的原型中继承属性。这个构造器函数的意义在于初始化对象,因此这个类型的函数调用创建实例。

当一个属性访问 myObject.myFunction 前拥有 new 关键词,那么 JavaScript 会执行构造器调用而不是方法调用。举个例子:new myObject.myFunction() 意味着首先这个函数会解析为一个属性访问函数extractedFunction = myObject.myFunction,然后用构造器创建一个新对象 new extractedFunction

4.1. 在构造函数调用中的 this

this is the newly created object in a constructor invocation

构造器调用的环境是新创建的对象。通过传递构造函数参数来初始化新建的对象,添加属性初始化值以及事件处理器。

让我们来验证以下这个例子的上下文环境:

function Foo () {  
  console.log(this instanceof Foo); // => true
  this.property = 'Default Value';
}
// 构造函数调用
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';
  }
}
// 构造函数调用
var barInstance = new Bar();  
barInstance.property; // => 'Default Value' 

当执行 new Bar() 时,JavaScript 创建了一个空对象并且它的上下文环境为 constructor 方法,因此添加属性的办法是使用 this 关键词:this.property = 'Default Value'

4.2. 陷阱:忘记添加 new 关键词

一些 JavaScript 函数创建实例,不仅仅可以使用构造器的形式调用也可以利用函数调用,下面是一个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+') 时,JavaScript 创建了两个相等的普通表达式对象。

但是使用函数调用创建对象会产生潜在的问题(包括工厂模式),当失去了 new 关键词,一些构造器会取消初始化对象。

以下例子描述了这个问题:

function Vehicle(type, wheelsCount) {  
  this.type = type;
  this.wheelsCount = wheelsCount;
  return this;
}
// 函数调用
var car = Vehicle('Car', 4);  
car.type;              // => 'Car'  
car.wheelsCount // => 4  
car === window  // => true  

Vehicle 是一个在对象上设置了 type 和 wheelsCount 属性的函数。

当执行了 Vehicle('Car', 4) 时,会返回对象 car,它拥有正确的属性值:car.type 指向Carcar.wheelsCount 指向 4,开发者会误以为这样创建初始化对象没有什么问题。
然而,当前执行的是函数调用,因此 this 指向的是 window 对象,所以它设置的属性其实是挂在 window对象上的,这样是完全错误的,它并没有创建一个新对象。

应该正确的执行方式是使用 new 关键词来保证构造器被正确调用:

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

// 函数调用,会报错。
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 调用 increment 函数。

两个方法最主要的区别为 .call() 接收一组参数,如 myFunction.call(thisValue, 'value1', 'value2'),而 .apply() 接收一串参数作为类数组对象传递,如 myFunction.apply(thisValue, ['value1', 'value2'])

5.1. 间接调用中的 this

this is the first argument of .call() or .apply() in an indirect invocation

很明显,在间接调用中,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'  

当函数执行需要特别指定上下文时,间接调用非常有用,它可以解决函数调用中的上下文问题(this 指向window 或者严格模式下指向 undefined),同时也可以用来模拟方法调用对象。

另一个实践例子为,在 ES5 中的类继承中,调用父级构造器。

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;
}
var myRabbit = new Rabbit('White Rabbit', 4);  
myRabbit; // { name: 'White Rabbit', countLegs: 4 }

Runner.call(this, name) 在 Rabbit 里间接调用了父级方法初始化对象。

6. 绑定函数调用

绑定函数调用是将函数绑定一个对象,它是一个原始函数使用了 .bind() 方法。原始绑定函数共享相同的代码和作用域,但是在执行时拥有不同的上下文环境。

方法 .bind(thisArg[, arg1[, arg2[, ...]]]) 接收第一个参数 thisArg 作为绑定函数在执行时的上下文环境,以及一组参数 arg1, arg2, ... 作为传参传入函数中。 它返回一个新的函数,绑定了 thisArg

下列代码创建了一个绑定函数并在之后被调用:

function multiply(number) {  
  'use strict';
  return this * number;
}
// 创建绑定函数,绑定上下文2
var double = multiply.bind(2);  
// 调用间接调用
double(3);  // => 6  
double(10); // => 20

multiply.bind(2) 返回一个新的函数对象 double,它绑定了数字 2multiply 和 double 函数拥有相同的代码和作用域。

对比方法 .apply() 和 .call(),它俩都立即执行了函数,而 .bind() 函数返回了一个新方法,绑定了预先指定好的 this ,并可以延后调用。

6.1. 绑定函数中的 this

this is the first argument of .bind() when invoking a bound function

.bind() 方法的作用是创建一个新的函数,执行时的上下文环境为 .bind() 传递的第一个参数,它允许创建预先设置好 this 的函数。

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

var numbers = {  
  array: [3, 5, 10],
  getNumbers: function() {
    return this.array;    
  }
};
// 创建一个绑定函数
var boundGetNumbers = numbers.getNumbers.bind(numbers);  
boundGetNumbers(); // => [3, 5, 10]  
// 从对象中抽取方法
var simpleGetNumbers = numbers.getNumbers;  
simpleGetNumbers(); // => undefined 或者严格模式下抛出错误

numbers.countNumbers.bind(numbers) 返回了绑定 numbers 对象的函数 boundGetNumbers,它在调用时的 this 指向的是 numbers 并且返回正确的数组对象。

.bind() 创建了一个永恒的上下文链并不可修改。一个绑定函数即使使用 .call() 或者 .apply()传入其他不同的上下文环境,也不会更改它之前连接的上下文环境,重新绑定也不会起任何作用。
只有在构造器调用时,绑定函数可以改变上下文,然而这并不是特别推荐的做法。

下面这个例子声明了一个绑定函数,然后试图更改其预定上下文的情况:

function getThis() {  
  'use strict';
  return this;
}
var 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. 箭头函数

箭头函数的设计意图是以精简的方式创建函数,并绑定定义时的上下文环境。

var hello = (name) => {  
  return 'Hello ' + name;
};
hello('World'); // => 'Hello World'  
// 保留偶数
[1, 2, 5, 6].filter(item => item % 2 === 0); // => [2, 6]

箭头函数使用了轻便的语法,去除了关键词 function 的书写,甚至当函数只有一个句子时,可以省去return 不写。

箭头函数是匿名的,意味着函数的属性 name 是一个空字符串 '',它没有一个词汇式的函数名,意味着不利于使用递归或者解除事件处理。

同时它不同于普通函数,它不提供 arguments 对象,在 ES6 中可以用另外的参数代替:

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 is the enclosing context where the arrow function is defined

箭头函数并不创建它自身执行的上下文,使得 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 或者严格模式下undefined)。因此,要使同样的代码可以正确运行就必须人工绑定上下文,即 setTimeout(function() {...}.bind(this))。使用箭头函数就可以省略这么详细的函数绑定,用更加干净简短的代码绑定函数。

如果箭头函数在最外层作用域定义,那么上下文环境将永远是全局对象,一般来说在浏览器中即为window

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

箭头函数一次绑定上下文后便不可更改,即使使用了上下文更改的方法:

var numbers = [1, 2];  
(function() {  
  var get = () => {
    console.log(this === numbers); // => true
    return this;
  };
  console.log(this === numbers); // => true
  get(); // => [1, 2]
  // 箭头函数使用 .apply() 和 .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

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 对象依旧存在在调用的上下文环境中。这是因为箭头函数拥有静态的上下文环境,不会因为不同的调用而改变。

this 指向的是 window,因此 this.hour 和 this.minutes 都是 undefined。方法返回的结果为:'undefined hours and 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';
};
var walkPeriod = new Period(2, 30);  
walkPeriod.format(); // => '2 hours and 30 minutes'  

walkPeriod.format() 是一个在对象中的方法调用,它的上下文环境为 walkPeriodthis.hours 指向2this.minutes 指向 30,因此可以返回正确的结果:'2 hours and 30 minutes'

8. 结论

因为函数调用会极大地影响到 this,所以从现在开始不要直接问自己:

this 是从哪里来的?

而是要开始思考:

当前函数是怎么被调用的?

遇到箭头函数时,考虑:

当箭头函数被定义时,this 是指向什么?

标签: javascript

目录

评论

*
*

正在加载验证码......

最新评论

  • 无评论