看过我以往文章的朋友知道我是个人推崇JavaScript 函数而尽量避免使用class
FreewheelLee:应该在JavaScript中使用Class吗?zhuanlan.zhihu.com
但是,人在江湖飘哪能不被社会毒打。当你所在的团队的代码是以class为核心的时候,你还是不得不去熟悉、了解甚至精通 JavaScript this / class / prototype。
this 是面向对象编程中非常常见的一个概念,本文将由浅入深,探讨JavaScript this 的相关知识。
函数 中 的 this 和 bind
当我们用字面量的形式创建一个JavaScript object
const cat = {
sound: '喵',
talk(){
console.log(this.sound); // this 指向当前对象 }}cat.talk(); // 输出: 喵
这个行为大家应该都不觉得奇怪,尤其是从Java过来的朋友。
场景一:
JavaScript最特别的特性之一是函数是一等公民,可以被当做变量、参数返回值等。于是我们尝试把 cat.talk 拿出来玩玩
const catTalkFunc = cat.talk; // 将函数赋值给变量
catTalkFunc(); // 输出:undefined
原因:
执行 cat.talk() 时,talk 更准确地说 是个属于cat这个对象的方法,它的this此时指向的是 cat 这个对象,所以this.soud指向的是 '喵'
而当 cat.talk 赋值给 catTalkFunc 时,就类似于
const catTalkFunc = function(){
console.log(this.sound);
}
执行 catTalkFunc() 时 this 不再指向 cat 对象。
划重点:JavaScript 函数 中的 this 指向的不是该函数定义的时候的上下文而是调用该函数的上下文
如果我们希望输出跟之前一样的结果,有以下三种选择
// 直接指定this并调用
catTalkFunc.call(cat);
catTalkFunc.apply(cat);
// 绑定this后产生一个新的函数
const catTalkFunc2 = catTalkFunc.bind(cat);
catTalkFunc2(); // 调用新函数
call 和 apply 都是 Function 的方法,第一个参数是打算指定的 this 上下文,后面的参数是传递给 catTalkFunc 的参数,但是本例中 catTalkFunc 不需要任何参数。这两个方法的更多细节可以参考官方文档 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call 和 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
我们重点看一下 bind 方法,bind 方法会产生一个新的函数 catTalkFunc2 ,直接调用 catTalkFunc2 就能输出: ”喵“
这是怎么做到的呢?这是不是违背了上面说的 ”JavaScript 函数 中的 this 指向的不是该函数定义的时候的上下文而是调用该函数的上下文“ —— 当然没有
bind 方法可以近似理解为这么实现(仅仅是模拟实现)
Function.prototype.newBind = function (thisArg) {
const func = this; // func 变量指向当前这个函数
return function () { // 返回新函数
const args = Array.prototype.slice.call(arguments);
func.apply(thisArg, args); // 使用 thisArg 作为 this上下文
}
}
即 利用高阶函数和 apply 方法,绑定好 this 并返回一个新的函数,而新的函数里并不依赖执行时的任何 this 上下文。
场景二:
接上面的例子
const dog = {
sound: '汪',
talk: cat.talk
}
dog.talk();
预测一下最后一行会输出什么? 是 ’喵‘,’汪‘ 还是 undefined ?
答案是:’汪‘ ,因为talk被调用时,this 上下文是 dog 对象
这边要强调的一点是 无论是场景一中 的 cat.talk 和 catTalkFunc 还是 场景二里的 dog.talk 其实并没有什么特别的处理 都只是同一个函数的引用 即
function(){
console.log(this.sound);
}
关于 this 和 bind 先小结一下:
- 在JavaScript 中 函数 是类似于 string、number、boolean 的值存在的,它们可以被赋给变量、可以做参数、可以做返回值。由于这种灵活性,在函数中的 this 只会在函数运行时才确定。
可以使用一个口语例子作为类比更好理解:
当我说”这里好热!“的时候,你并不知道”这里“是哪里。
假如我在车里这么说,我指的是车里热;当我在房间里这么说,我指的是房间很热;当我在户外这么说,我指的是当地的天气很热 —— ”这里“的具体指向只取决于我在什么地方说出这句话。 - bind 方法
有时我们想避免这种歧义,就可以使用 bind 显示地指定这个函数的 this 上下文。bind 会返回一个已经绑定好 this 的新函数。
道理我都懂,但为什么鸽子这么大
this 到底指向了什么
当 函数在对象中定义且作为对象方法运行时,函数中的 this 自然指向了对象,如上面的例子
const cat = {
sound: '喵',
talk(){
console.log(this.sound); // this 指向当前对象
}
}
cat.talk(); // 输出: 喵
当函数在没有对象上下文的时候被直接执行,函数中的 this 指向的是全局对象 —— 在 Node 环境中对应的就是 global 对象,在 浏览器环境中对应的就是 window 对象
可以做以下测试验证这个说法
const cat = {
myThis(){
return this;
}
}
console.log(cat.myThis() === cat); // true
const theThis = cat.myThis;
console.log(theThis() === cat); // false
console.log(theThis() === global); // 在 Node 环境中为 true
console.log(theThis() === window); // 在 浏览器 环境中为 true
常规函数中的 this 没有作用域链
const foo = {
sound: 'fffff',
say(){
console.log(this.sound);
} ,
bar(){
function inner(){
console.log("this.sound inner " + this.sound);
}
inner();
}
}
foo.say(); // fffff
foo.bar(); // 输出什么?
上面的最后一行代码会输出什么呢?
如果你认为是 'this.sound inner fffff' ,那么你的思路应该是 inner() 在被执行时,虽然外层是 bar() 函数不是对象,但再往外一层就是 foo 对象了,所以 this.sound 指向的是 foo.sound
这种思路是典型的变量作用域链的思路 —— 对于其他 JavaScript 变量而言,的确是一层层函数往外这么找的,如下面这个例子
function aFunc() {
const aVar = 1;
function bFunc() {
const bVar = 2;
function cFunc() {
const cVar = 3;
console.log(aVar);
}
cFunc();
}
bFunc();
}
aFunc(); // 输出 1 —— 从 cFunc 到 bFunc 到 aFunc 一层层寻找 aVar 的定义
但是,this 比较特殊,它不遵循这种链式的规则 —— 如果函数执行时不处于对象上下文中,那么this就直接指向全局对象。
可以对代码做以下修改来打印出 this.sound inner fffff 的结果
const foo = {
sound: 'fffff',
say() {
console.log(this.sound);
},
bar() {
const _this = this; // 使用一个临时变量存放 this 引用
function inner() {
console.log("this.sound inner " + _this.sound); // 使用 _this 变量
}
inner();
}
}
foo.say(); // fffff
foo.bar(); // this.sound inner fffff
箭头函数的 this
到目前为止,本文提到的函数都是常规函数。
在 ES2015(即ES6)之后,JavaScript引入一个新特性叫 箭头函数 (arrow function),它内部的 this 实现跟常规函数有很大的不同:
箭头函数没有自己的 this,箭头函数中的 this 直接绑定了创建(定义)该箭头函数时的 this
因此上面的代码也可以这么改
const foo = {
sound: 'fffff',
say() {
console.log(this.sound);
},
bar() {
const inner = () => { // 箭头函数
console.log("this.sound inner " + this.sound);
}
inner();
}
}
foo.say(); // fffff
foo.bar(); // this.sound inner fffff
由于 inner 函数是个箭头函数,且在定义 inner 函数时,this 指向的是 foo 对象,因此inner的this 就绑定了 foo 对象
补充:
关于 箭头函数的 this,有一位读者有困惑跟我讨论了一下,他提出了以下的例子
const cap = {
sound: 'capcap',
bar: foo.bar,
}
cap.bar(); // 输出 this.sound inner capcap
这样是不是推翻了 “箭头函数中的 this 直接绑定了创建(定义)该箭头函数时的 this” 呢?
当然不是。上面的代码等同于
const cap = {
sound: 'capcap',
bar: function () {
const inner = () => {
console.log("this.sound inner " + this.sound);
}
inner();
},
}
那箭头函数 inner 是什么时候被创建的呢? —— 是在 bar() 方法执行的时候创建的,因此 inner 的 this 指向的就是 bar() 方法上下文中的 this —— 也就是 cap 对象
再看一个特别的例子:
const bird = {
color: 'white',
describe: () => {
console.log("bird is " + this.color);
}
}
bird.describe(); // 输出 bird is undefined
看到这里,你是豁然开朗了还是更加困惑了?
解释: 创建 describe 箭头函数时,bird 还在构建中,因此箭头函数指向的是全局对象而不是 bird 对象。
const birdGarden = {
location: 'Shanghai',
bird: {
color: 'white',
describe: () => {
console.log("bird is " + this.color + " in " + this.location);
}
}
}
birdGarden.bird.describe(); // 输出 bird is undefined in undefined
跟上例同理
另外,上面提到的 Function 的 apply, call 和 bind 方法都对箭头函数无效!
bird.describe.call(bird); // bird is undefined
bird.describe.apply(bird); // bird is undefined
bird.describe.bind(bird)(); // bird is undefined
因此,如果你想给箭头函数赋 this 的值,一种方法是利用高阶函数 —— 在目标this上下文中定义一个函数,在函数中创建箭头函数
const bird = {
color: 'white',
describe() {
return () => {
console.log("bird is " + this.color);
}
}
}
bird.describe()(); //bird is white
解析: bird.describe 方法的this指向的是 bird 对象,bird.describe 方法执行时会创建一个匿名的箭头函数,这个箭头函数的 this 也因此指向了 bird 对象,所以这个箭头函数的执行结果是 "bird is white"
另一种方法是在构造器函数中创建箭头函数
function Bird() {
this.color = 'white';
this.describe = () => {
console.log("bird is " + this.color);
}
}
const b = new Bird();
b.describe(); // bird is white
Bird 函数作为构造器,创建 describe 箭头函数时,上下文的this就是即将构建的 Bird 实例(构造器特性),因此能输出正确结果。
如果你已经开始晕头转向了,那么恭喜你 品尝到了 JavaScript的噩梦之 this 的滋味!也初步领略到了 JavaScript 诡异的面向对象机制。
本文先从这些简单的例子介绍 JavaScript this 的一些基础知识,在之后的文章会结合prototype、构造器函数、class关键字 进一步阐释。
关于 JavaScript 的 this 基础知识今天就分享到这,如果对你有帮助,欢迎点赞、喜欢、收藏三连!
有任何问题和建议可以留言或私信我。
推荐阅读:
FreewheelLee:应该在JavaScript中使用Class吗?zhuanlan.zhihu.com
FreewheelLee:什么?JavaScript不用class也能实现设计模式!zhuanlan.zhihu.com
参考链接:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
来源:oschina
链接:https://my.oschina.net/u/4350320/blog/4837772