A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

© 不二晨 金牌黑马   /  2018-12-14 09:32  /  736 人查看  /  2 人回复  /   0 人收藏 转载请遵从CC协议 禁止商业使用本文

要说 JavaScript 这门语言最容易让人困惑的知识点,this 关键词肯定算一个。JavaScript 语言面世多年,一直在进化完善,现在在服务器上还可以通过 node.js 来跑 JavaScript。显然,这门语言还会活很久。

所以说,我一直相信,如果你是一个 JavaScript 开发者或者说 web 开发者,学好 JavaScript 的运作原理以及语言特点肯定对你以后大有好处。

开始之前
在开始正文之前,我强烈推荐你先掌握好下面的知识:

变量作用域和作用域提升
JavaScript 的函数
闭包
如果没有对这些基础知识掌握踏实,直接讨论 JavaScript 的 this 关键词只会让你感到更加地困惑和挫败。

我为什么要学 this?
如果上面的简单介绍没有说服你来深入探索 this 关键词,那我用这节来讲讲为什么要学。

考虑这样一个重要问题,假设开发者,比如 Douglas Crockford (译者注:JavaScript 领域必知牛人),不再使用 new 和 this,转而使用完完全全的函数式写法来做代码复用,会怎样?

事实上,基于 JavaScript 内置的现成的原型继承功能,我们已经使用并且将继续广泛使用 new 和 this 关键词来实现代码复用。

理由一,如果只能使用自己写过的代码,你是没法工作的。现有的代码以及你读到这句话时别人正在写的代码都很有可能包含 this 关键词。那么学习怎么用好它是不是很有用呢?

因此,即使你不打算在你的代码库中使用它,深入掌握 this 的原理也能让你在接手别人的代码理解其逻辑时事半功倍。

理由二,拓展你的编码视野和技能。使用不同的设计模式会加深你对代码的理解,怎么去看、怎么去读、怎么去写、怎么去理解。我们写代码不仅是给机器去解析,还是写给我们自己看的。这不仅适用于 JavaScript,对其他编程语言亦是如此。

随着对编程理念的逐步深入理解,它会逐渐塑造你的编码风格,不管你用的是什么语言什么框架。

就像毕加索会为了获得灵感而涉足那些他并不是很赞同很感兴趣的领域,学习 this 会拓展你的知识,加深对代码的理解。

什么是 this ?

在我开始讲解前,如果你学过一门基于类的面向对象编程语言(比如 C#,Java,C++),那请将你对 this 这个关键词应该是做什么用的先入为主的概念扔到垃圾桶里。JavaScript 的 this 关键词是很不一样,因为 JavaScript 本来就不是一门基于类的面向对象编程语言。

虽说 ES6 里面 JavaScript 提供了类这个特性给我们用,但它只是一个语法糖,一个基于原型继承的语法糖。

this 就是一个指针,指向我们调用函数的对象。

我难以强调上一句话有多重要。请记住,在 Class 添加到 ES6 之前,JavaScript 中没有 Class 这种东西。Class 只不过是一个将对象串在一起表现得像类继承一样的语法糖,以一种我们已经习惯的写法。所有的魔法背后都是用原型链编织起来的。

如果上面的话不好理解,那你可以这样想,this 的上下文跟英语句子的表达很相似。比如下面的例子

Bob.callPerson(John);

就可以用英语写成 “Bob called a person named John”。由于 callPerson() 是 Bob 发起的,那 this 就指向 Bob。我们将在下面的章节深入更多的细节。到了这篇文章结束时,你会对 this 关键词有更好的理解(和信心)。

执行上下文
执行上下文 是语言规范中的一个概念,用通俗的话讲,大致等同于函数的执行“环境”。具体的有:变量作用域(和 作用域链条,闭包里面来自外部作用域的变量),函数参数,以及 this 对象的值。

引自: Stackoverflow.com

记住,现在起,我们专注于查明 this 关键词到底指向哪。因此,我们现在要思考的就一个问题:

是什么调用函数?是哪个对象调用了函数?
为了理解这个关键概念,我们来测一下下面的代码。

var person = {
name: “Jay”,
greet: function() {
console.log("hello, " + this.name);
}
};
person.greet();
谁调用了 greet 函数?是 person 这个对象对吧?在 greet() 调用的左边是一个 person 对象,那么 this 关键词就指向 person,this.name就等于 “Jay”。现在,还是用上面的例子,我加点料:

var greet = person.greet; // 将函数引用存起来;
greet(); // 调用函数
你觉得在这种情况下控制台会输出什么?“Jay”?undefined?还是别的?

正确答案是 undefined。如果你对这个结果感到惊讶,不必惭愧。你即将学习的东西将帮助你在 JavaScript 旅程中打开关键的大门。

this 的值并不是由函数定义放在哪个对象里面决定,而是函数执行时由谁来唤起决定。

对于这个意外的结果我们暂且压下,继续看下去。(感觉前后衔接得不够流畅)

带着这个困惑,我们接着测试下 this 三种不同的定义方式。

找出 this 的指向
上一节我们已经对 this 做了测试。但是这块知识实在重要,我们需要再好好琢磨一下。在此之前,我想用下面的代码给你出个题:

var name = “Jay Global”;
var person = {
name: ‘Jay Person’,
details: {
name: ‘Jay Details’,
print: function() {
return this.name;
}
},
print: function() {
return this.name;
}
};
console.log(person.details.print()); // ?
console.log(person.print()); // ?
var name1 = person.print;
var name2 = person.details;
console.log(name1()); // ?
console.log(name2.print()) // ?
console.log() 将会输出什么,把你的答案写下来。如果你还想不清楚,复习下上一节。

准备好了吗?放松心情,我们来看下面的答案。

答案和解析
person.details.print()
首先,谁调用了 print 函数?在 JavaScript 中我们都是从左读到右。于是 this 指向 details 而不是 person。这是一个很重要的区别,如果你对这个感到陌生,那赶紧把它记下。

print 作为 details 对象的一个 key,指向一个返回 this.name 的函数。既然我们已经找出 this 指向 details ,那函数的输出就应该是 ‘Jay Details’。

person.print()
再来一次,找出 this 的指向。print() 是被 person 对象调用的,没错吧?

在这种情况,person 里的 print 函数返回 this.name。this 现在指向 person 了,那 ‘Jay Person’ 就是返回值。

console.log(name1)
这一题就有点狡猾了。在上一行有这样一句代码:

var name1 = person.print;
如果你是通过这句来思考的,我不会怪你。很遗憾,这样去想是错的。要记住,this 关键词是在函数调用时才做绑定的。name1() 前面是什么?什么都没有。因此 this 关键词就将指向全局的 window 对象去。

因此,答案是 ‘Jay Global’。

name2.print()
看一下 name2 指向哪个对象,是 details 对象没错吧?

所以下面这句会打印出什么呢?如果到目前为止的所有小点你都理解了,那这里稍微思考下你就自然有答案了。

console.log(name2.print()) // ??
答案是 ‘Jay Details’,因为 print 是 name2 调起的,而 name2 指向 details。

词法作用域
你可能会问:“什么是词法作用域?”

逗我呢,我们不是在探讨 this 关键词吗,这个又是哪里冒出来的?好吧,当我们用起 ES6 的箭头函数,这个就要考虑了。如果你已经写了不止一年的 JavaScript,那你很可能已经碰到箭头函数。随着 ES6 逐渐成为现实标准,箭头函数也变得越来越常用。

JavaScript 的词法作用域 并不好懂。如果你 理解闭包,那要理解这个概念就容易多了。来看下下面的小段代码。

// outerFn 的词法作用域
var outerFn = function() {
var n = 5;
console.log(innerItem);
// innerFn 的词法作用域
var innerFn = function() {
var innerItem = “inner”; // 错了。只能坐着电梯向上,不能向下。
console.log(n);
};
return innerFn;
};
outerFn()();
想象一下一栋楼里面有一架只能向上走的诡异电梯。

建筑的顶层就是全局 windows 对象。如果你现在在一楼,你就可以看到并访问那些放在楼上的东西,比如放在二楼的 outerFn 和放在三楼的 window 对象。

这就是为什么我们执行代码 outerFn()(),它在控制台打出了 5 而不是 undefined。

然而,当我们试着在 outerFn 词法作用域下打出日志 innerItem,我们遇到了下面的报错。请记住,JavaScript 的词法作用域就好像建筑里面那个只能向上走的诡异电梯。由于 outerFn 的词法作用域在 innerFn 上面,所以它不能向下走到 innerFn 的词法作用域里面并拿到里面的值。这就是触发下面报错的原因:

test.html:304 Uncaught ReferenceError: innerItem is not defined
at outerFn (test.html:304)
at test.html:313
this 和箭头函数
在 ES6 里面,不管你喜欢与否,箭头函数被引入了进来。对于那些还没用惯箭头函数或者新学 JavaScript 的人来说,当箭头函数和 this 关键词混合使用时会发生什么,这个点可能会给你带来小小的困惑和淡淡的忧伤。那这个小节就是为你们准备的!

当涉及到 this 关键词,箭头函数 和 普通函数 主要的不同是什么?

答案:

箭头函数按词法作用域来绑定它的上下文,所以 this 实际上会引用到原来的上下文。

引自:hackernoon.com

我实在没法给出比这个更好的总结。

箭头函数保持它当前执行上下文的词法作用域不变,而普通函数则不会。换句话说,箭头函数从包含它的词法作用域中继承到了 this 的值。

我们不妨来测试一些代码片段,确保你真的理解了。想清楚这块知识点未来会让你少点头痛,因为你会发现 this 关键词和箭头函数太经常一起用了。

示例
仔细阅读下面的代码片段。

var object = {
data: [1,2,3],
dataDouble: [1,2,3],
double: function() {
console.log(“this inside of outerFn double()”);
console.log(this);
return this.data.map(function(item) {
console.log(this); // 这里的 this 是什么??
return item * 2;
});
},
doubleArrow: function() {
console.log(“this inside of outerFn doubleArrow()”);
console.log(this);
return this.dataDouble.map(item => {
console.log(this); // 这里的 this 是什么??
return item * 2;
});
}
};
object.double();
object.doubleArrow();
如果我们看执行上下文,那这两个函数都是被 object 调用的。所以,就此断定这两个函数里面的 this 都指向 object 不为过吧?是的,但我建议你拷贝这段代码然后自己测一下。

这里有个大问题:

arrow() 和 doubleArrow() 里面的 map 函数里面的 this 又指向哪里呢?

上一张图已经给了一个大大的提示。如果你还不确定,那请花5分钟将我们上一节讨论的内容再好好想想。然后,根据你的理解,在实际执行代码前把你认为的 this 应该指向哪里写下来。在下一节我们将会回答这个问题。

回顾执行上下文
这个标题已经把答案泄露出来了。在你看不到的地方,map 函数对调用它的数组进行遍历,将数组的每一项传到回调函数里面并把执行结果返回。如果你对 JavaScript 的 map 函数不太了解或有所好奇,可以读读这个了解更多。

总之,由于 map() 是被 this.data 调起的,于是 this 将指向那个存储在 data 这个 key 里面的数组,即 [1,2,3]。同样的逻辑,this.dataDouble 应该指向另一个数组,值为 [1,2,3]。

现在,如果函数是 object 调用的,我们已经确定 this 指向 object 对吧?好,那来看看下面的代码片段。

double: function() {
return this.data.map(function(item) {
console.log(this); // 这里的 this 是什么??
return item * 2;
});
}
这里有个很有迷惑性的问题:传给 map() 的那个匿名函数是谁调用的?答案是:这里没有一个对象是。为了看得更明白,这里给出一个 map 函数的基本实现。

// Array.map polyfill
if (Array.prototype.map === undefined) {
Array.prototype.map = function(fn) {
var rv = [];
for(var i=0, l=this.length; i<l; i++)
rv.push(fn(this));
return rv;
};
}
fn(this)); 前面有什么对象吗?没。因此,this 关键词指向全局的 windows 对象。那,为什么 this.dataDouble.map 使用了箭头函数会使得 this 指向 object 呢?

我想再说一遍这句话,因为它实在很重要:

箭头函数按词法作用域将它的上下文绑定到 原来的上下文

现在,你可能会问:原来的上下文是什么?问得好!

谁是 doubleArrow() 的初始调用者?就是 object 对吧?那它就是原来的上下文

this 和 use strict
为了让 JavaScript 更加健壮及尽量减少人为出错,ES5 引进了严格模式。一个典型的例子就是 this 在严格模式下的表现。你如果想按照严格模式来写代码,你只需要在你正在写的代码的作用域最顶端加上这么一行 “use strict;”。

记住,传统的 JavaScript 只有函数作用域,没有块作用域。举个例子:

function strict() {
// 函数级严格模式写法
‘use strict’;
function nested() { return ‘And so am I!’; }
return "Hi! I’m a strict mode function! " + nested();
}
function notStrict() { return “I’m not strict.”; }
代码片段来自 Mozilla Developer Network。

不过呢,ES6 里面通过 let 关键词提供了块作用域的特性。

现在,来看一段简单代码,看下 this 在严格模式和非严格模式下会怎么表现。在继续之前,请将下面的代码运行一下。

(function() {
“use strict”;
console.log(this);
})();
(function() {
// 不使用严格模式
console.log(this);
})();
正如你看到的,this 在严格模式下指向 undefined。相对的,非严格模式下 this 指向全局变量 window。大部分情况下,开发者使用 this ,并不希望它指向全局 window 对象。严格模式帮我们在使用 this 关键词时,尽量少做搬起石头砸自己脚的蠢事。

举个例子,如果全局的 window 对象刚好有一个 key 的名字和你希望访问到的对象的 key 相同,会怎样?上代码吧:

(function() {
// “use strict”;
var item = {
document: “My document”,
getDoc: function() {
return this.document;
}
}
var getDoc = item.getDoc;
console.log(getDoc());
})();
这段代码有两个问题。

this 将不会指向 item。
如果程序在非严格模式下运行,将不会有错误抛出,因为全局的 window 对象也有一个名为 document 的属性。
在这个简单示例中,因为代码较短也就不会形成大问题。

如果你是在生产环境像上面那样写,当用到 getDoc 返回的数据时,你将收获一堆难以定位的报错。如果你代码库比较大,对象间互动比较多,那问题就更严重了。

值得庆幸的是,如果我们是在严格模式下跑这段代码,由于 this 是 undefined,于是立刻就有一个报错抛给我们:

test.html:312 Uncaught TypeError: Cannot read property ‘document’ of undefined at getDoc (test.html:312) at test.html:316 at test.html:317

明确设置执行上下文
先前假定大家都对执行上下文不熟,于是我们聊了很多关于执行上下文和 this 的知识。

让人欢喜让人忧的是,在 JavaScript 中通过使用内置的特性开发者就可以直接操作执行上下文了。这些特性包括:

bind():不需要执行函数就可以将 this 的值准确设置到你选择的一个对象上。还可以通过逗号隔开传递多个参数,如 func.bind(this, param1, param2, …) 。
apply():将 this 的值准确设置到你选择的一个对象上。第二个参数是一个数组,数组的每一项是你希望传递给函数的参数。最后,执行函数。
call():将 this 的值准确设置到你选择的一个对象上,然后想 bind 一样通过逗号分隔传递多个参数给函数。如:print.call(this, param1, param2, …)。最后,执行函数。
上面提到的所有内置函数都有一个共同点,就是它们都是用来将 this 关键词指向到其他地方。这些特性可以让我们玩一些骚操作。只是呢,这个话题太广了都够写好几篇文章了,所以简洁起见,这篇文章我不打算展开它的实际应用。

重点:上面那三个函数,只有 bind() 在设置好 this 关键词后不立刻执行函数。

什么时候用 bind、call 和 apply
你可能在想:现在已经很乱了,学习所有这些的目的是什么?

首先,你会看到 bind、call 和 apply 这几个函数到处都会用到,特别是在一些大型的库和框架。如果你没理解它做了些什么,那可怜的你就只用上了 JavaScript 提供的强大能力的一小部分而已。

如果你不想了解一些可能的用法而想立刻读下去,当然了,你可以直接跳过这节,没关系。

下面列出来的应用场景都是一些具有深度和广度的话题(一篇文章基本上是讲不完的),所以我放了一些链接供你深度阅读用。未来我可能会在这篇终极指南里面继续添加新的小节,这样大家就可以一次看过瘾。

方法借用
柯里化
偏函数应用
依赖注入
如果我漏掉了其他实践案例,请留言告知。我会经常来优化这篇指南,这样你作为读者就可以读到最丰富的内容。

阅读高质量的开源代码可以升级你的知识和技能。

讲真,你会在一些开源代码上看到 this 关键词、call、apply 和 bind 的实际应用。我会将这块结合着其他能帮你成为更好的程序员的方法一起讲。

在我看来,开始阅读最好的开源代码是 underscore。它并不像其他开源项目,如 d3,那样铁板一块,而是内部代码相互比较独立,因而它是教学用的最佳选择。另外,它代码简洁,文档详细,编码风格也是相当容易学习。

JavaScript 的 this 和 bind
前面提到了,bind 允许你明确设定 this 的指向而不用实际去执行函数。这里是一个简单示例:

var bobObj = {
name: “Bob”
};
function print() {
return this.name;
}
// 将 this 明确指向 “bobObj”
var printNameBob = print.bind(bobObj);
console.log(printNameBob()); // this 会指向 bob,于是输出结果是 “Bob”
在上面的示例中,如果你把 bind 那行去掉,那 this 将会指向全局 window 对象。

这好像很蠢,但在你想将 this 绑定到具体对象前你就必须用 bind 来绑定。在某些场景下,我们可能想从另一个对象中借用一些方法。举个例子,

var obj1 = {
data: [1,2,3],
printFirstData: function() {
if (this.data.length)
return this.data[0];
}
};
var obj2 = {
data: [4,5,6],
printSecondData: function() {
if (this.data.length > 1)
return this.data[1];
}
};
// 在 obj1 中借用 obj2 的方法
var getSecondData = obj2.printSecondData.bind(obj1);
console.log(getSecondData()); // 输出 2
在这个代码片段里,obj2 有一个名为 printSecondData 的方法,而我们想将这个方法借给 obj1。在下一行

var getSecondData = obj2.printSecondData.bind(obj1);
通过使用 bind ,我们让 obj1 可以访问 obj2 的 printSecondData 方法。

【转载】仅作分享,侵删

2 个回复

倒序浏览
奈斯
回复 使用道具 举报
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马