本文主要总结自《JavaScript 语言精粹》、部分总结自《JavaScript 高级程序设计》以及自己的经验

四种调用模式

在 JavaScript 中,this 的值取决于调用模式,有四种调用模式,分别是方法调用模式、函数调用模式、构造器调用模式、Apply、call 调用模式。

方法调用模式

var name = "window",
lzh = {
name: "lzh",
sayName: function(){
alert(this.name); // 输出 "lzh"
}
} lzh.sayName();

函数调用模式

var name = "window",
lzh = {
name: "lzh",
sayName: function(){
innerFunction();
function innerFunction(){
alert(this.name);
}
return function(){
alert(this.name);
}
}
} lzh.sayName()();

上面这段代码 alert 的均是 window,从上面可以看出,不管外部环境的 this 是不是 window,通过函数调用模式调用的函数,this 指向 window。

来看一段 ES6 箭头函数中的 this (上面提到箭头函数基本纠正了设计上的错误)

var name = 'window';
var lzh = {
name: 'lzh',
sayName: function(){
return ()=> {
console.log(this.name);
}
}
} var iny = {
name: 'iny'
} lzh.sayName().apply(iny); // 输出 lzh

其实转换成 ES5 是这么干的:

var name = 'window';
var lzh = {
name: 'lzh',
sayName: function(){
var _this = this;
return function(){
console.log(_this.name);
}
}
} var iny = {
name: 'iny'
} lzh.sayName().apply(iny); // lzh

但如果ES6 中这么写

var name = "window";
var lzh = {
name: 'lzh',
sayName: () => {
console.log(this.name)
}
} var iny = {
name: 'iny'
}
lzh.sayName(); // window
lzh.sayName.apply(iny); // window

转换成 ES5 却是这样的

var name = "window";
var _this = this;
var lzh = {
name: 'lzh',
sayName: function() {
console.log(_this.name)
}
}; var iny = {
name: 'iny'
}
lzh.sayName(); // window
lzh.sayName.apply(iny); // window
// 有点失望

构造器调用模式

var Person = function(name){
this.name = name;
} Person.prototype.getName = function(){
return this.name;
} var lzh = new Person("lzh"); console.log(lzh.getName()); // lzh

Apply、call 调用模式

function sum(num1, num2){
console.log(this);
return num1 + num2;
}
function callSum1(num1, num2){
return sum.apply(this, arguments); // 传入 arguments 对象
}
function callSum2(num1, num2){
return sum.apply(null, [num1, num2]); // 传入数组
}
function callSum3(num1, num2){
return sum.call(null, num1, num2); // 一个一个地传递参数
}
alert(callSum1(10,10)); //20
alert(callSum2(10,10)); //20
alert(callSum3(10,10)); //20

从上面的代码可以看出,apply 的第一个参数是一个改变 sum 函数的 this 的值,但这里不论传进去的是 window 还是 null,内部 console.log 出来的都是 window 对象,还可以看出,apply 的第二个参数要么是 arguments、要么是一个数组。call 从第二个参数开始,就要一个一个的传递参数,而不能传递数组或arguments。

单从上面的代码,不能很好的看出 apply、call 的长处,既然 call 能设置 this,那么就能复用其它对象的方法,比如下面这个:

var lzh = {
name: 'lzh',
say: function(something){
alert(something + this.name);
}
} var iny = {
name: 'iny'
} lzh.say.apply(iny, ['hi, I am ']); // 输出 hi I am iny
  • iny 对象没有 say 方法,但是又希望复用 lzh 的 say 方法,那么就可以用 apply 或 call
  • 这样还是不能很明显的看出 call、apply 的优越性,举个现实点的例子,如何把 arguments 转换成数组,因为 arguments 不是数组,是一个 Array like 的对象,就是有下标元素,可以通过 arguments[0]、arguments[1] 来访问它的元素,但它没有数组的各种方法,比如 popshiftslice 等,操作 arguments 会不大方便,所以我们希望把 arguments 转换成 数组。如果我们大概明白 Array.prototype.slice 的实现原理的话,我们可以利用这个方法将 arguments 转换成数组。
  • 第一步,讲一下 Array.prototype.slice 简易版的大概实现原理(原版应该是使用 C++ 实现的,功能和性能上都比这个简易版的要好):
Array.prototype.slice = function(start, end){
var newArray = [];
if(start >= 0 && end <= this.length){
for(var i = start; i < end; i++){
newArray.push(this[i]);
}
}
return newArray;
} var testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(testArray.slice(0, 2)); // [1, 2]
  • 从上面我们可以看出,假如我们用 Array.prototype.slice.apply(arguments, 0, arguments.length),就相当于把 slice 内部的 this 换成了 arguments; 就可以把 arguments[0]、arguments[1]...等 push 到一个新数组,这样就可以成功的把 arguments 转换成数组了,于是就可以利用数组的各种方法操作参数。当然,这里只是简易地重写了一遍 slice,真实的 slice 可以不传递这里的第三个参数,默认从 0 截取到末尾。
  • apply、call 在实现函数柯里化、对象继承上也有很大的作用,这里不详细展开。

匿名函数中的 this

  • 在网上看了很多关于 this 的博客,都有介绍到匿名函数中的 this 指向 window 对象,但这种说法是不正确的,关键还是要看怎么调用(就是前面介绍的4中调用方式),比如下面的代码
var name = "window",
lzh = function(){
return function(){
//这里是匿名函数,但是 this 的值只有在调用的时候才能确定
alert(this.name);
}
} var iny = {
name: 'iny',
sayName: lzh()
}
lzh()(); // window
iny.sayName(); // iny

从上面可以看出,this 的值在调用的时候决定

  • 还有就是事件处理程序里面的 this
    在 DOM0 级、DOM2 级的事件处理程序中(onclick/addEventListener),this 指向绑定事件的那个元素,虽然不知道浏览器内部的具体实现,但可以猜测它是由 DOM 对象以方法调用的,属于 DOM 对象的方法,而在 IE 旧版本的实现中,attachEvent 指定的事件处理程序的调用模式应该是函数调用模式,所以 this 指向 window。如有错误,还请指出。
  • setTimeout、setInterval 里面的 this 也指向 window,这个应该还是由调用模式决定的。

下面看几道题目

  • 题目1
var name = "window",
lzh = {
name: "lzh",
sayName: function(){
alert(this.name);
alert(name);
}
} lzh.sayName();

题目1先 alert lzh,再 alert window,alert window 的原因是:sayName 实际上是一个闭包,它的活动对象里有 this(指向 lzh 对象),但没有 name,所以它往父级作用域链寻找 name, 于是找到了全局作用域的变量对象中的 windw,所以 alert window如果想了解闭包的更多内容,可以点这里

  • 题目2
var name = "The Window";

var object = {
name : "My Object", getNameFunc : function(){
return function(){
return this.name;
};
}
}; alert(object.getNameFunc()());

滑动查看答案:alert 的是 "The Window"

  • 题目3
var name = "window",
person = {
name: 'lzh',
getName: function(){
return this.name;
}
} console.log(person.getName());
console.log((person.getName)());
console.log((person.getName = person.getName)());

滑动查看答案:输出顺序:lzh、lzh、window

  • 如果本文对您有帮助,不妨点赞一下,您的鼓励是我的动力,我会更努力地写出好文章。
04-20 19:18