JavaScript 里的 'this' 的一般解释
本文旨在帮助自己和大家理解 JS 里的 this
, 翻译、整理并改写自本人关注的一个博主 Dmitri Pavlutin,原文链接如下:
https://dmitripavlutin.com/gentle-explanation-of-this-in-javascript/
欢迎大家批评指正、共同进步。
1. this
的神秘
长久以来,this
关键字对我来说都是神秘。
在 Java, PHP 或者其他标准语言里,this
是 class 方法里当前对象的实例。this
不能在方法外调用,这样一种简单的规则不会造成困惑。
在 JavaScript 里情况有所不同,this
是函数调用时候的上下文,JS 存在着4种函数调用的方式:
- 常规调用(function invocation):
alert('Hello World!')
- 作为方法调用(method invocation):
console.log('Hello World!')
- 作为构造函数调用(constructor invocation):
new RegExp('\\d')
- 间接调用(indirect invocation):
alert.call(undefined, 'Hello World!')
每一种调用方式都会对 this
有影响,因此 this
表现得和开发者的预期有所不同。
另外,严格模式(strict mode)也会对执行上下文有所影响。
理解 this
的关键在于拥有一幅关于函数调用和它是如何影响上下文的清晰图景。
本文着眼于函数调用方式的解释,函数的调用方式是如何影响 this
的,并且演示了一些在判断 this
时常见的陷阱。
在开始之前,让我们先来熟悉一些概念:
- 函数的调用(Invocation)指的是执行组成函数体的代码,例如,对于
parseInt
函数的调用是parseInt('15')
。 - 调用时的上下文(Context)指的是函数体内
this
的值。 - 函数的作用域(Scope)指的是函数体内可访问的变量和函数的集合。
2~5是对4种调用方式的详细介绍
2. 常规调用
一个常规调用的简单例子:
function hello(name) {
return 'Hello ' + name + '!';
}
// 常规调用
const message = hello('World');
常规调用不能是属性访问,例如 obj.myFunc()
, 这是一个方法调用,再比如 [1,5].join(',')
是一个方法调用而不是常规调用,请记住它们之间的区别
一个更高级的例子是立即执行函数(IIFE), 这也是常规调用
// IIFE
const message = (function(name) {
return 'Hello ' + name + '!';
})('World');
2.1 常规调用时的 this
在常规调用里,this
是全局对象。
全局对象取决于执行环境,在浏览器,全局对象就是 window
.
让我们通过以下的例子来检验:
function sum(a, b) {
console.log(this === window); // => true
this.myNumber = 20; // 给全局对象添加了 'myNumber' 属性
return a + b;
}
// sum() 以常规的方式调用
// sum 里的 this 是全局对象(window)
sum(15, 16); // => 31
window.myNumber; // => 20
当 this
在任何函数作用域之外使用时(最外层的域:全局执行上下文),它也等于全局对象
console.log(this === window); // => true
this.myString = 'Hello World!';
console.log(window.myString); // => 'Hello World!'
<!-- 在一个 html 文件里 -->
<script type="text/javascript">
console.log(this === window); // => true
</script>
2.2 严格模式下常规调用里的 this
严格模式下,常规调用里的 this
是 undefined
.
一个采用严格模式的常规调用的例子:
function multiply(a, b) {
'use strict'; // 开启严格模式
console.log(this === undefined); // => true
return a * b;
}
// multiply() 开启了严格模式的常规调用
// multiply() 里的 this 是 undefined
multiply(2, 5); // => 10
值得注意的是,严格模式不仅会影响当前的函数作用域,也会影响嵌套定义的函数的作用域
function execute() {
'use strict'; // 此处开启了严格模式
function concat(str1, str2) {
// 此处也自动开启了严格模式
console.log(this === undefined); // => true
return str1 + str2;
}
concat('Hello', ' World!'); // => "Hello World!"
}
execute();
2.3 陷阱:在嵌套定义的内层函数里的 this
一个常见的错误是认为内层函数的 this
和外层函数的 this
一样,事实上,内层函数(箭头函数除外)的 this
只取决于它的调用方式,而不是外层函数的 this
.
为了让 this
变为我们期待的值,可以通过将内层函数的调用方式改为间接调用(使用 .call()
或 .apply()
, 见第5节),或者创建一个预先绑定了 this
的函数(使用 .bind()
, 详见第6节)。
下面的例子用以计算两个数的和:
const numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // => true
function calculate() {
// this 是 window 或 undefined(严格模式下)
console.log(this === numbers); // => false
return this.numberA + this.numberB;
}
return calculate();
}
};
numbers.sum(); // => NaN 或 throws TypeError (严格模式下)
numbers.sum();
是一个方法调用(详见第3节),因此,this
等于 numbers
. calculate()
函数定义在 sum()
内,你可能会认为调用 calculate()
时,this
也是 numbers
.
calculate()
是一个常规调用而不是方法调用,因此其 this
是 window
或 undefined
(严格模式下),即便外层函数的 this
是 numbers 对象,对 calculate
也没影响。
为了解决这个问题,一个方法是手动地改变 calculate()
的执行上下文,例如 calculate.call(this)
(一种间接调用的方式,详见第5节)。
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
calculate.call(this)
和常规调用一样调用 calculate
函数,但是采用传入的第一个参数作为 calculate
的 this
另外一种稍好的方式,是使用箭头函数:
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
箭头函数的 this
是词法绑定的,换句话说,使用 numbers.sum()
的 this
值。
3. 方法调用
方法 是存储于一个对象属性里的函数,例如:
const myObject = {
// helloMethod 是一个方法
helloMethod: function() {
return 'Hello World!';
}
};
const message = myObject.helloMethod();
helloMethod
是 myObject
的一个方法。使用属性访问器访问该方法 myObject.helloMethod
.
方法调用依赖属性访问的方式调用函数(obj.myFunc()
或者obj['myFunc']()
),而常规的函数调用不需要(myFunc()
),牢记这个区别很重要。
const words = ['Hello', 'World'];
words.join(', '); // 方法调用
const obj = {
myMethod() {
return new Date().toString();
}
};
obj.myMethod(); // 方法调用
const func = obj.myMethod;
func(); // 常规函数调用
parseFloat('16.6'); // 常规函数调用
isNaN(0); // 常规函数调用
3.1 方法调用里的 this
在方法调用里,this
是拥有该方法的对象。
让我们创建一个拥有增加数值的方法的对象:
const calc = {
num: 0,
increment() {
console.log(this === calc); // => true
this.num += 1;
return this.num;
}
};
// 方法调用,this 指代 calc
calc.increment(); // => 1
calc.increment(); // => 2
让我们来考察另外一个 case, 一个对象从它的原型继承了一个方法,当这个被继承的方法在对象上调用时,this
仍是对象本身(而非原型)。
const myDog = Object.create({
sayName() {
console.log(this === myDog); // => true
return this.name;
}
});
myDog.name = 'Milo';
// 方法调用 this 是 myDog
myDog.sayName(); // => 'Milo'
Object.create()
创建了一个新对象 myDog
, 并且将自己的第一个参数设为 myDog
的原型,myDog
继承了 sayName
方法。
当 myDog.sayName()
被调用时,myDog
是执行上下文(亦即 this
)。
在 ES2015 的 class
语法里,方法调用的上下文依然是实例本身:
class Planet {
constructor(name) {
this.name = name;
}
getName() {
console.log(this === earth); // => true
return this.name;
}
}
const earth = new Planet('Earth');
// 方法调用。上下文是 earth
earth.getName(); // => 'Earth'
3.2 陷阱:把方法从对象里分离出来
一个方法可以从对象里剥离出来置于一个独立的变量,例如 const alone = myObj.myMethod
. 当该方法被单独调用时,alone()
, 脱离了原有的对象,你可能会认为 this
是原有的对象。
事实上,当方法从对象里剥离出来并且单独调用时,这就是一个常规的函数调用,this
是 window
或者 undefined
(严格模式下)。
下面的例子,定义了一个名为 Pet
的构造函数,并且实例化了一个对象:myCat
, 然后 setTimout()
在1秒后打印出 myCat
对象的信息:
function Pet(type, legs) {
this.type = type;
this.legs = legs;
this.logInfo = function() {
console.log(this === myCat); // => false
console.log(`The ${this.type} has ${this.legs} legs`);
}
}
const myCat = new Pet('Cat', 4);
// 打印出 "The undefined has undefined legs"
// 或者严格模式下报 TypeError
setTimeout(myCat.logInfo, 1000);
你可能会认为 setTimeout(myCat.logInfo, 1000)
会调用 myCat.logInfo()
, 并且打印出关于 myCat
对象的信息。
不幸的是,这个方法是从对象里抽离出来并且作为一个参数被传递,setTimout(myCat.logInfo)
, 上面的例子和下面的等效:
setTimout(myCat.logInfo);
// 等同于
const extractedLogInfo = myCat.logInfo;
setTimout(extractedLogInfo);
当这个独立出来的函数 logInfo
被调用时,这就只是一次常规的调用,this
是全局对象或者 undefined
(严格模式下),因此 myCat
的信息没有被正确打印出来。
一个预先绑定了 this
的函数可以解决此问题(以下是简介,详见第6节):
function Pet(type, legs) {
this.type = type;
this.legs = legs;
this.logInfo = function() {
console.log(this === myCat); // => true
console.log(`The ${this.type} has ${this.legs} legs`);
};
}
const myCat = new Pet('Cat', 4);
// 创建一个绑定了 `this` 的函数
const boundLogInfo = myCat.logInfo.bind(myCat);
// 打印出 "The Cat has 4 legs"
setTimeout(boundLogInfo, 1000);
myCat.logInfo.bind(myCat)
返回了一个新的函数,该函数在被以常规方式调用时,this
指向 myCat
.
另外,也可使用箭头函数来达到相同目的:
function Pet(type, legs) {
this.type = type;
this.legs = legs;
this.logInfo = () => {
console.log(this === myCat); // => true
console.log(`The ${this.type} has ${this.legs} legs`);
};
}
const myCat = new Pet('Cat', 4);
// 打印出 "The Cat has 4 legs"
setTimeout(myCat.logInfo, 1000);
4. 构造函数调用
一些构造函数调用的例子:
new Pet('cat', 4)
, new RegExp('\\d')
构造函数时,this
是新创建的对象。
下面的例子定义了一个函数 Country
, 并以构造函数的方式调用:
function Country(name, traveled) {
this.name = name ? name : 'United Kingdom';
this.traveled = Boolean(traveled); // transform to a boolean
}
Country.prototype.travel = function() {
this.traveled = true;
};
// 构造函数调用
const france = new Country('France', false);
france.travel(); // Travel to France
new Country('France', false)
是 Country
函数的构造函数式调用,这样的调用创建了一个新对象,该对象的 name
属性为 'France'.
从 ES2015 开始,我们还可以用 class
的语法定义上述例子:
class City {
constructor(name, traveled) {
this.name = name;
this.traveled = false;
}
travel() {
this.traveled = true;
}
}
// 构造函数调用
const paris = new City('Paris', false);
paris.travel();
需要注意的是,当一个属性访问器前面跟着 new
关键字时,JS 表现的是构造函数调用,而不是方法调用。
例如:new myObject.myFunction()
, 这个函数先是被使用属性访问器剥离出来,extractedFunction = myObject.myFunction
, 然后再以构造函数的方式被调用来创建新对象
5. 间接调用
间接调用是指,当函数以如下的方式被调用:
myFunc.apply(thisArg, [arg1, arg2, ...])
或者 myFunc.call(thisArg, arg1, arg2, ...)
间接调用的情况下,this
是传入 apply()
或者 call()
的第一个参数
6. 预先绑定了 this
的函数
形如 myFunc.bind(thisArg[, arg1, arg2, ...)
, 接收第一个参数作为指定的 this
, 返回一个新的函数,举例如下:
function multiply(number) {
'use strict';
return this * number;
}
// 创建了一个预先绑定了 this 的函数 double,用数字 2 作为 this 的值
const double = multiply.bind(2);
// 调用 double 函数
double(3); // => 6
double(10); // => 20
需要注意的是,使用 .bind()
创建出来的函数无法再使用 .call()
或者 .apply()
修改上下文,即使重新调用 .bind()
也无法改变。
7. 箭头函数
箭头函数没有属于自己的执行上下文,它从外层函数那获取 this
, 换句话说,箭头函数的 this
是词法绑定的
8. 总结
因为函数的执行方式对 this
有很大的影响,所以从现在开始,不要问自己“这个this
”是从哪儿来的,而是问自己,“这个函数式是怎么样调用的”,对于箭头函数,问自己“在箭头函数定义的外层函数里,this
是什么”,拥有这样的 mindset 会让你工作时少很多头痛
JavaScript 里的 'this' 的一般解释的更多相关文章
- 如何才能通俗易懂的解释javascript里面的"闭包"?
看了知乎上的话题 如何才能通俗易懂的解释javascript里面的‘闭包’?,受到一些启发,因此结合实例将回答中几个精要的答案做一个简单的分析以便加深理解. 1. "闭包就是跨作用域访问变量 ...
- JavaScript里的依赖注入
JavaScript里的依赖注入 我喜欢引用这句话,“程序是对复杂性的管理”.计算机世界是一个巨大的抽象建筑群.我们简单的包装一些东西然后发布新工具,周而复始.现在思考下,你所使用的语言包括的一些内建 ...
- javascript闭包—围观大神如何解释闭包
闭包的概念已经出来很长时间了,网上资源一大把,本着拿来主意的方法来看看. 这一篇文章 学习Javascript闭包(Closure) 是大神阮一峰的博文,作者循序渐进,讲的很透彻.下面一一剖析. 1. ...
- JavaScript里值比较的方法
JavaScript里值比较的方法 参考资料 一张图彻底搞懂JavaScript的==运算 toString()和valueof()方法的区别 Object.is 和 == 与 === 不同 == 运 ...
- javascript里for循环的一些事情
今天在给一个学妹调她的代码BUG时,她的问题就是在一个for循环里不清楚流程的具体流向,所以导致了页面怎么调都是有问题,嗯确实你如果不清楚语句流向很轻易就会出问题,所以说for循环不会用或者说用的不恰 ...
- Javascript里,想把一个整数转换成字符串,字符串长度为2
Javascript里,想把一个整数转换成字符串,字符串长度为2. 想把一个整数转换成字符串,字符串长度为2,怎么弄?比如 1 => "01"11 => " ...
- javascript里的循环语句
前序:我一直对于for跟for..in存在一种误解,我觉得for都能把事情都做了,为啥还要for...in...这玩意了,有啥用,所以今天就说说JavaScript里的循环语句. 循环 要计算1+2+ ...
- Safari 里的javascript 里不能用submit作为函数名
Safari 里的javascript 里不能用submit作为函数名, 这样写的时候,怎么也运行不了JeasyUI的onSubmit的function, 改个名就可以了.而在chrome下面就没问题 ...
- JavaScript 里,$ 代表什么?/JQuery是什么语言?/html中用link标签引入css时的中 rel="stylesheet"属性?/EL表达式是什么?
JavaScript 里,$ 代表什么? 比如说我写一个mouseover事件: $(document).ready(function(){ $("p").mouseover(fu ...
随机推荐
- Linux上大文件切割以及批量并发处理
一.环境说明 某次项目需求中,在Linux上有批文本文件,文件文件都有几个G大,几千万行的数据.无论在Linux和Windows打开这么大的文件,基本上打开要卡半天,更别说编辑. 因此想到使用spli ...
- Linux-CPU优化之平均负载率
一.平均负载率定义 平均负载是指单位时间内,系统处于可运行状态 和不可中断状态 的平均进程数,也就是平均活跃进程数,它和CPU 使用率并没有直接关系. 可运行状态的进程:是指正在使用 CPU 或者正在 ...
- ensp上防火墙的实现
使用ensp模拟器中的防火墙(USG6000V)配置NAT(网页版)一.NAT介绍NAT(Network Address Translation,网络地址转换):简单来说就是将内部私有地址转换成公网地 ...
- 挖到一款免费好用的web报表插件
最近公司项目需要用到报表,公司领导要求我来调研下报表工具.开始的时候了解了目前市场上功能强大,占有率高的两款报表工具,帆软报表和润乾报表,这两款报表工具功能比较强大,覆盖的行业较广,基本能满足所有的报 ...
- ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]
<诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...
- java this 用法详解
一.JAVA提供了一个很好的东西,就是 this 对象,它可以在类里面来引用这个类的属性和方法. 代码例子: public class ThisDemo { String name="Mic ...
- 『无为则无心』Python日志 — 64、Python日志模块logging介绍
目录 1.日志的作用 2.为什么需要写日志 3.Python中的日志处理 (1)logging模块介绍 (2)logging模块的四大组件 (3)logging日志级别 1.日志的作用 从事与软件相关 ...
- Windows server 2008 R2 多用户远程桌面配置详解(超过两个用户)
转至:https://www.jb51.net/article/139542.htm 注意:一下是针对win2008 server r2的操作 1. 创建三个本地管理员测试用户 user01 use ...
- 反射、静态代理、动态代理(jdk、cglib)
一.反射 反射在之前的文章中详细的解释过了,简单概括就是:可以动态的获取到一个类内部的所有的信息,动态的去创建对象和使用对象以及可以操作对象的属性和方法. 二.代理 首先解释一下代理:使用一个代理对象 ...
- 进程&线程(一)——multiprocessing,threading
本节内容为①进程线程的基础知识:②在Python的实现方法: 学习总结自: 一文看懂Python多进程与多线程编程(工作学习面试必读) - 知乎 multiprocessing 官方文档 1.进程线程 ...