在后边的学习中经常会遇到this,相信很多人和我一样,到现在为止,对this的具体指向还是很茫然
谁调用,this就指向谁
这就像一本保真的降龙十八掌内功心经一样,流传甚广
但是没有招法,该从何下手呢?
江湖中各种方法各有优势,先分享一下我觉得最好用的方式(the best),并找了其他两个有代表性的,一同分享给大家。
1、I Don‘t Know JS
在看完了诸多前辈的博客之后,可以说是一个跟斗跳到了云里雾里(玩笑玩笑)。
回过头来想,啥玩意是this(此为自己对You-Dont-Know-JS的理解和搬运,也可以去看原文,在下边有链接)
通俗来说,Javascript 是一个文本作用域的语言, 一个变量的作用域,在写这个变量的时候确定,是静态的。 而this 关键字就是为了在 JS 中加入动态作用域(就是在自己地盘上不够得劲了,我要出去转转)而做的努力。用大家 难懂的话来说就是:
this
不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this
绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连。当一个函数被调用时,会建立一个称为执行环境的活动记录。这个记录包含函数是从何处(调用栈 —— call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间 引用
this
。
这句话什么意思呢?就是说,this不是我们在实例化一个window对象,声明一个函数.......等时侯绑定的,他是为了实现动态,在这些东西运行时绑定的
划重点,强烈建议记住这句话,是在运行时绑定。。。。。。
我们在声明一个函数后马上执行,像这样
var a = 1
//声明时,this没有绑定,假装那里就是个马赛克
function print(){
console.log(this.a)
}
//在此处调用了,我们很明显知道这是全局环境下,此时的call-tack就是全局环境,那么this就是全局对象
print()
有人说这个太简单了,当然好解释,那么看个复杂的:
//此时,this没被绑定
var point = {
x: 0,
moveTo: function(x, y) {
console.log(this)
}
}
//调用绑定this
point.moveTo()
使用point.moveTo()对函数进行了调用,通俗讲就是point对象通过它的key去访问它的value,所以很容易理解,函数是在point对象处(call-site)被调用的,因此,this指的是point。
所以根据书中的内容梳理总结了一个更好记的方法
具体来说有四种, 优先级有低到高分别如下:
默认的 this 绑定, 就是说 在一个函数中使用了 this, 但是没有为 this 绑定对象. 这种情况下, 非严格模式下, this 就是全局变量 (Node 环境中的 global, 浏览器环境中的 window)
隐式绑定: 使用 obj.foo() 这样的语法来调用函数的时候, 函数 foo 中的 this 绑定到 obj 对象.
this
绑定最常让人沮丧的事情之一,就是当一个 隐含绑定 丢失了它的绑定,这通常意味着它会退回到 默认绑定, 根据strict mode
的状态,其结果不是全局对象就是undefined
。
const foo = { bar: 10, fn: function() { console.log(this) console.log(this.bar) } } var fn1 = foo.fn fn1()
- 强制绑定: foo.call(obj, ...), foo.apply(obj,[...]), foo.bind(obj,...),this就是第一个参数 obj
- 构造绑定: new foo() , 这种情况, 无论 foo 是否做了绑定, 都要创建一个新的对象, 然后 foo 中的 this 引用这个对象
那么就有以下的流程(按顺序判断)
函数是通过
new
被调用的吗(new 绑定)?如果是,this
就是新构建的对象。
var bar = new foo()
函数是通过
call
或apply
被调用(明确绑定),甚至是隐藏在bind
硬绑定 之中吗?如果是,this
就是那个被明确指定的对象。有call之类的
var bar = foo.call( obj2 )
函数是通过对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,
this
就是那个对象。被调用时,前边有东西
var bar = obj1.foo()
否则,使用默认的
this
(默认绑定)。如果在strict mode
下,就是undefined
,否则是global
对象。被调用时前边没东西
var bar = foo()
课后习题
var point = {
x: 0,
y: 0,
moveTo: function(x, y) {
//定义时没有绑定
function moveX(x) {
this.x = x
}
//调用时。按顺序判断是4,即默认绑定,所以输出undefined
console.log(moveX(x))
}
}
point.moveTo()
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o3 = {
text: 'o3',
fn: function() {
//如果在这个地方有this,那就是O3这个obj
var fn = o1.fn
//在这调用,此时绑定this,前边没东西,所以指的是全局对象,因此是undefined
return fn()
}
}
console.log(o1.fn()) //o1,这个太简单,没什么要解释的
//对于这个,还是看调用位置,虽然fn在这调用了,但是fn里没有this啊
console.log(o3.fn())
画个示意图(巧记而已):
2、看阮一峰的博客后有感
他把this的使用分为以下几个部分,这也是网上大多数的方法
情况一:纯粹函数的使用(为了跟后边的构造函数区分开,这里应该是被当作方法使用时?)
//这里的this就代表全局对象,所以运行结果是1
var x = 1
function print(){
console.log(this.x)
}
print() //1
//所以在函数里直接使用的话,this指向的都是全局对象吗
function print2(){
let x = 1
console.log(this.x)
}
print() // undefined
//let 在全局环境下的声明和var也不一样吗?见下边的分析
let y = 1
function print2(){
console.log(this.y)
}
print2() // undefined
情况二:作为对象方法的调用
function print(){
console.log(this.x)
}
obj = {}
obj.x = 1
obj.y = print
obj.y() //1
此时this指向的是上级对象
有一种情况
function print(){
console.log(this.x)
}
obj1 = {x:2}
obj2 = {x:5}
obj1.y = print
obj2.a = obj1.y
obj2.a() //5
虽然这里是赋值操作,但最终调用的是obj2,谁调用this就是谁
情况三:作为构造函数调用
function Test{
this.x = 1
console.log(this.x)
}
let newObj = new Test() //1
此时this指向的是我们new的对象
这个的过程可以去看一下构造函数的相关知识arr的reverse方法是在哪里定义? - 知乎 (zhihu.com)
情况四:apply调用
apply()
是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this
指的就是这第一个参数。
var x = 0;
function test() {
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply() // 0
apply()
的参数为空时,默认调用全局对象。因此,这时的运行结果为0
,证明this
指的是全局对象。
如果把最后一行代码修改为
obj.m.apply(obj); //1
此时,this就代表apply的第一个参数,也就是obj,对于apply、call和bind其实都是去改变传入函数的this
比如:很多时候,我们想让this指向自己,为了代码的方便,比如在迭代中时
function foo(num) {
console.log( "foo: " + num )
// 追踪 `foo` 被调用了多少次
this.count++
}
foo.count = 0
for (var i=0; i<10; i++) {
if (i > 5) {
foo(i)
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// `foo` 被调用了多少次?
console.log( foo.count ) // 0 -- 这他妈怎么回事……?
但最后发现是0,学了上述的this指向(不管是方方的,还是网络上常用的),我们可以理解到,在for循环里,foo()每次调用时的this指向的是全局对象(在web环境下指代的是window,node环境下是global),所以this.count相当于给全局对象创建了个属性,那么怎么更改他呢?
function foo(num) {
console.log( "foo: " + num )
// 追踪 `foo` 被调用了多少次
this.count++
}
foo.count = 0
for (var i=0; i<10; i++) {
if (i > 5) {
//利用call去更改
foo.call(foo,i)
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// `foo` 被调用了多少次?
console.log( foo.count ) //4
注:
作为补充,有些知识点需要知道
一、执行上下文
执行上下文(以下简称“上下文”)的概念在 JavaScript 中是颇为重要的。变量或函数的上下文决定 了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object) , 而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台 处理数据会用到它。
全局上下文是最外层的上下文,在浏览器中,全局上下文的对象就是我们 常说的window对象,因此所有通过 var 定 义的全局变量和函数都会成为 window 对象的属性和方法
二、区分一下let、var、const
首先这三个关键字都是声明变量的方式
-
var关键字
- 使用方法
//声明一个变量,并赋值 var message = 'a' //可以被这么重写,但是不推荐 message = 'b'
-
声明作用域
使用 var 操作符定义的变量会成为包含它的函数的局部变量,在函数内部定义变量,在函数退出时会被立即销毁,这在ES5之前常用,比如避免一些变量被全局使用
function sum (a){ var b = 1 return a + b } console.log(sum(1)) //2 console.log(b) //b is not defined
不过他可以这样转换为全局变量
//相当于前边有个var b b的值是undefined //var b function sum (a){ b = 1 return a + b } console.log(sum(1)) //2 console.log(b) //1
但这样的方法不推荐,因为很多时候你并不知道你有没有在外边使用过这个变量,如果贸然使用此种方法就会把它重写,带来不方便
//在代码中使用过 var b = 8 //.... function sum (a){ b = 1 return a + b } console.log(sum(1)) //2 console.log(b) //1
从以上我们可以看出来,var关键字声明的范围是函数作用域和全局作用域
-
声明提升
见函数内容相关博客
var的这些特性带来很多不方便
-
声明提升带来的变量覆盖
var a = 'hello' function sayHello(){ console.log(a) if(false){ var a = 'no' } } sayHello() //undefined
-
计数的变量成为全局变量
//在全局作用域下的if 或 for 或 {} 中var声明的变量都是全局变量,for 循环定义的迭代变量会渗透到循环体外部 for(var i = 1; i < 5 ; i++){ console.log(i) } console.log(i) //5
-
let 关键字
-
声明范围
这是它和var的最大区别,let 的声明范围是块级作用域(在ES6 出现)
何为块级作用域,简单理解,他只在它被使用的那个上下文是可用的,在孩子或者父亲那里都不可以
if (true){ var name = 'BlueMiao' console.log(name) } console.log(name) //BlueMiao if (true){ let name1 = 'BlueMiao1' console.log(name1) } console.log(name1) // is not defined
由于它的作用域范围,因此它支持嵌套重复定义
-
let 声明的变量不会在作用域中被提升。
-
使用 let 在全局作用域中声明的变量不会成为 window 对象的属性
-
在使用 var 声明变量时,由于声明会被提升,JavaScript 引擎会自动将多余的声明在作用域顶部合 并为一个声明。因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同 时也就不可能在没有声明的情况下声明它。
所以有下边的情况
var name var name var name = 'Blue' console.log(name) //Blue let name let name //报错
-
-
const 关键字
与let基本相同 ,只是必须声明时要立即初始化
const a = 1 const a //报错 const a = 2 a = 1 //报错 //但是当变量是对象时,改变不会报错 const a = [] a.push('a')
《javascript高级程序语言设计》中这么说
- 不使用 var
- const 优先,let 次之
3、读方应杭的博客有感
此时再读方方老师的博客,感觉这更应该是种好的记忆方法
一个转换公式,记就完事了,没有为什么
func(p1, p2) //等价于
func.call(undefined, p1, p2)
obj.child.method(p1, p2)// 等价于
obj.child.method.call(obj.child, p1, p2)
对于一些特殊的情况
比如new、箭头函数(跟外边环境一样)、事件(事件发生的主体,比如按钮点击的按钮)再进行单独记忆
4、引用
this - JavaScript | MDN (mozilla.org)
你怎么还没搞懂 this? - 知乎 (zhihu.com)
(建议收藏)原生JS灵魂之问, 请问你能接得住几个?(上) - 知乎 (zhihu.com)
JavaScript 的 this 原理 - 阮一峰的网络日志 (ruanyifeng.com)
Javascript 的 this 用法 - 阮一峰的网络日志 (ruanyifeng.com)
You-Dont-Know-JS/ch2.md at 1ed-zh-CN · getify/You-Dont-Know-JS (github.com)