This
概述
this的绑定在初学js时总会让人产生一些疑惑。我们总是可以在网上见到很多的博客说,谁调用的这个函数,那this就指向谁。但是如果我们涉及到了回调函数的情况下,这样的说法似乎就变的有些儿陌生。特别是回调函数的函数实例分为全局函数,类成员方法,箭头函数。
现在我们尝试对this的不同情况进行分类. 以及得到一个通用的结论
概括性的来说. JavaScript 中,普通的函数调用方式有三种: 直接调用、方法调用和 new 调用. 除此之外, 还有一些特殊的调用方式, 比如通过 bind() 将函数绑定到对象之后再进行调用, 通过 call(), apply() 进行调用等.
而 es6 引入了箭头函数之后,箭头函数调用时, 其 this 指向又有所不同. 下面就来分析这些情况下的 this 指向.
不过在ArkTS的新版中,不推荐(禁止)使用 通过 bind() 将函数绑定到对象之后再进行调用、通过 call()、apply() 进行调用
所以我们这里就分析其他的一些调用情况.
结论
其实对于this的确定, 主要是分辨函数调用时的情况 大部分情况下, 就是判断函数被调用时, 其前面有没有对象. 没有对象就是直接调用, 则this为undefined 如果有对象那就是该对象. 不用管函数是怎么进行传递的, 就看咋调用, 下面的大部分内容就是对不同情况下的举例
如果是箭头函数, 就判断定义这个箭头函数的上下文中this是啥
一起加油 :D
函数单独调用-默认绑定
[!note] 单独调用, 也可以认为是直接调用. 就是通过 函数名( ... ) 这种形式的调用 这种直接调用换个说法就是: 如果函数在没有被任何东西访问的情况下被调用, this 将是 undefined
严格模式下为this默认为undefined. 非严格模式下为GlobalThis.
浏览器环境下为window需要注意的是, 并不是说直接调用需要函数在全局作用域下进行调用. 而是在任何作用域下, 通过 函数名(...) 的方式进行的调用, 都可以称为直接调用
例如下面的这些情况
全局函数下调用
function foo01() {
console.log(this)
}
// 1. 全局环境下直接调用
foo01() // window \ undefined
获取方法引用进行调用
如果对象的方法引用了外部函数,当我们将这个方法单独赋值给一个函数对象的时候,这时候调用就是单独调用
function foo01() {
console.log(this)
}
const obj = {
bar: foo01
}
// obj.bar(); // {bar: f ..。} 这种不是直接调用 不是 函数名(..。)的形式
const fn1 = obj.bar
fn1() // window \ undefined
注意,这里如果直接基于对象调用方法,this是当前对象。obj是一个类的实例。只不过这个类没有名字(匿名类?) 在进行调用的时候,会自动的给方法传一个this过去
方法内部调用外部函数
function foo() {
console.log("foo function")
console.log(this)
}
const obj1 = {
name: 'obj1',
bar() {
foo() // 直接调用 函数名(...) 的形式,所以foo函数内部的this为 Undefined \ window
}
}
obj1.bar()
const obj2 = {
name: 'obj2',
baz() {
obj1.bar() // 不是直接调用,所以obj1.bar函数内部的this是obj1。但是我们输出的foo函数的执行语句,所以是 window \ undefined
}
}
obj2.baz()
都是输出 window \ undefined
全局变量引用方法调用
const obj2 = {
bar() {
console.log("bar call")
console.log(this)
}
}
// obj2.bar() // {bar: f ... } 这个不是直接调用
const fn2 = obj2.bar
fn2() // window \ undefined。// 符合直接调用的规则,所以fn2引用的函数中的this为 window \ undefined
其实这种方式和上面那种方式类似,只不过我们这里类的方法不是引用外部函数,而是自己定义了函数
[!note] 这段代码也可以体会我们常说JS中的this是在运行时确定, 而不是编译时
我们的bar()方法是定义在对象中的, 但是因为直接调用, 它的this是undefined而不是obj2
全局函数嵌套调用
function foo02() {
console.log('foo02')
console.log(this)
}
function foo03() {
console.log('foo03')
console.log(this)
foo02() // 符合直接调用的规则
}
function foo04() {
console.log('foo04')
console.log(this)
foo03() // 符合直接调用的规则
}
foo04() // 内部所有的函数的this都是 window \ undefined
闭包
const obj3 = {
bar() {
return function () {
console.log('bar函数的返回函数')
console.log(this)
}
}
}
const fn3 = obj3.bar()
fn3() // window \ undefined
// 这2种方式,结果一致
obj3.bar()() // 可以理解为具体解析时,是按上面那种步骤来的。也是 函数(...) 的形式进行调用
注意: 闭包要看它的执行环境,如果是箭头函数,那么要向上查找作用域; 如果是普通函数,还要看它是不是作为了独立函数调用,此时是 window \ Undefined
所以看了这么多的例子之后,我们大概也对直接调用有了一点感觉了吧。在确定this是什么, 我们不关注函数在哪里定义的, 而是函数如何被调用的.
方法调用-隐式绑定
方法调用是指通过对象来调用其方法函数,它是 对象.方法函数(...) 这样的调用形式。这种情况下,函数中的 this 指向调用该方法的对象。但是,同样需要注意 bind() 的影响。
这种方式也是我们常说的,谁调用该方法,谁就是this
// 隐式绑定
function foo() {
console.log("foo function")
console.log(this)
}
const obj1 = {
name: 'obj1',
foo: foo
}
// this = obj1{name: 'obj1',foo: f ... }
obj1.foo() // 符合方法调用的约定,所以foo方法内部的this就是obj1 即使foo是引用了其他地方的函数
const obj2 = {
name: 'obj2',
bar() {
console.log("obj2 bar call")
console.log(this)
}
}
// obj2{name: 'obj2',bar: f ... }
obj2.bar() // 最常见的方法调用了。直接通过实例.方法的形式
const obj3 = {
name: 'obj3',
baz: obj2.bar
}
// obj3{name: 'obj3',baz: f ... }
obj3.baz() // 注意,我们说过谁调用这个方法,this就是指向谁。所以这里的baz方法中的this就是obj3,即使baz是引用了其他类的方法
以上三种都属于隐式绑定,他们都是通过对象调用,this就指向了该对象
从上面几个例子我们再次可以看出,函数中的this和定义在哪里没有关系,和如何进行调用有关系 例如我们的bar方法是定义在obj2对象中的,但是我们通过obj3进行调用的时候,他的this就是obj3
其他情况
由于ArkTS中不推荐使用显式绑定(bind,apply 等),所以这里直接介绍其他情况的this. 当然他们的使用也很简单.
回调函数中this
因为js对于函数良好的支持,所以我们常常将函数作为变量进行传递,而作为参数的函数我们通常称其为回调函数
- 全局函数和全局函数
function foo() {
console.log('foo function call')
console.log(this) // undefined \ window
}
function bar(f) {
console.log('bar func call')
console.log(this) // undefined \ window
f() // 符合直接调用的约定
}
bar(foo)
- 全局函数和方法
function foo() {
console.log('foo function call')
console.log(this) // undefined \ window
}
class A {
name = 'A'
bar = function (f) {
console.log('bar method call')
console.log(this) // A{ name: A,bar: f ..。}
f() // 符合直接调用的约定
}
}
const a = new A()
a.bar(foo)
- 同一个类中的方法和方法
class A {
name = 'A'
foo = function () {
console.log('foo method call')
console.log(this) // undefined
}
bar = function (f) {
console.log('bar method call')
console.log(this) // A{ name: A,foo: f,bar: f ..。}
f() // 符合直接调用的约定
}
}
const a = new A()
a.bar(a.foo)
注意,类的主体会执行在严格模式下,即便没有写
"use strict"指令也一样。所以类中的方法的this不会有window了。
- 不同类中的方法和方法
class A {
name = 'A'
bar = function (f) {
console.log('bar method call')
console.log(this) // A{ name: A,bar: f ..。}
f() // 符合直接调用的约定
}
}
class B {
foo () {
console.log('b class foo method call')
console.log(this) // undefined
}
}
const a = new A()
const b = new B()
a.bar(b.foo)
- 类的静态方法和普通方法
class A {
name = 'A'
static bar = function (f) { // 加上了static修饰
console.log('bar method call')
console.log(this) // 类本身
f() // 符合直接调用的约定, 前面没有任何的调用者
}
}
class B {
foo () {
console.log('b class foo method call')
console.log(this) // undefined
}
}
const b = new B()
A.bar(b.foo)
- 异步事件中的this指向
class A {
name = 'A'
foo() {
console.log('foo method call')
console.log(this) // undefined \ window
}
}
const a = new A()
setTimeout(a.foo, 1) // 回调函数, 我们无法得知setTimeout底层的代码, 但是通过之前的了解. 这里很大概率也是直接调用, 所以foo中this为undefined \ window
由 setTimeout() 执行的代码是从一个独立于调用 setTimeout 的函数的执行环境中调用的。
为被调用的函数设置 this 关键字的通常规则适用,如果你没有在调用中或用 bind 设置 this,它将默认为 window(或 global)对象。它将与调用 setTimeout 的函数的 this 值不一样。
通常情况下, 我们会直接传递箭头函数, 而箭头函数的this就是根据我们定义的位置确定其this的.
- js有该特性, 其实在ts中也遵从. 自然ArkTS也遵从, 看一下ts的代码
所以在mdn的文档上有这样的一句话
[!note] 当一个函数作为回调函数传递时,this 的值取决于如何调用回调,这由 API 的实现者决定。
回调函数通常以 undefined 作为 this 的值被调用(直接调用,而不附加到任何对象上),这意味着如果函数是在非严格模式,this 的值会是全局对象(globalThis)。
这在迭代数组方法、Promise() 构造函数等例子中都是适用的。
所以我们给ForEach方法传递函数的时候,函数中的this一般是undefined
箭头函数的this
这箭头函数没有自己的 this 绑定。箭头函数中使用的 this,其实是直接包含它的那个函数或函数表达式中的 this
初次接触,我感觉不用将箭头函数想象的过于复杂。我们看一段将ES6转译成低版本的js的代码
可以发现,其实箭头函数在变成低版本的代码时,就是创建了一个匿名的函数,然后该函数中的this就是直接被替换为了一个内部变量,而这个内部变量的值来源于在定义时获取的this,它运行之前this已经确定了.
所以判断箭头函数的this指向时,需要判断其外部的this指向。
这也就是MDN上说的
在箭头函数中,this 保留了闭合词法上下文的 this 值。 换句话说,当对箭头函数求值时,语言不会创建一个新的 this 绑定。
箭头函数在其周围的作用域上创建一个 this 值的闭包,这意味着箭头函数的行为就像它们是“自动绑定”的——无论如何调用,this 都绑定到函数创建时的值(在上面的例子中,是全局对象)。
在其他函数内部创建的箭头函数也是如此:它们的 this 值保持为闭合词法上下文的 this。
不管在什么情况下使用箭头函数,它本身是没有绑定 this 的,它用的是直接外层函数(即包含它的最近的一层函数或函数表达式)绑定的 this。
在将箭头函数作为回调函数的时候,注意分辨调用函数的方式,是直接调用还是什么
- 全局函数和箭头函数
function bar(f) {
console.log('bar func call')
console.log(this) // undefined \ window
// 如果是箭头函数,则函数内部this和该方法的this无关
f()
}
bar(() => {
console.log('arrow function call')
console.log(this) // 获取全局环境的this。所以是window
})
- 箭头函数和全局函数
function bar() {
console.log('bar func call')
console.log(this) // undefined \ window
}
foo = (f) => {
f() // 符合 直接调用约定 所以this为 undefined \ window
}
foo(bar)
- 方法中传递箭头函数
class A {
name = 'A'
foo = function (bar) {
console.log('foo method call')
console.log(this) // {name: 'A',foo: f ..。}
bar()
}
}
const a = new A()
a.foo(() => {
console.log('arrow function call')
console.log(this) // 和对象没有关系,当前它在全局环境中定义的。所以this是 undefined \ window
})
- 箭头函数中传递方法
class A {
name = 'A'
foo = function () {
console.log('foo method call')
console.log(this) // 类中,处于严格模式,所以为 undefined
}
}
const a = new A()
const fn = (f) => {
console.log('arrow function call')
console.log(this) // undefined \ window
f() // 符合直接调用,所以f函数内部的this为 undefined \ window
}
fn(a.foo)
- 箭头函数作为方法
class A {
name = 'A'
foo = () => {
console.log('arrow method call')
// this 是当前类的实例,即使是直接调用,它也是类的实例。因为箭头函数的this在定义时确定,而非运行时
console.log(this) // A {name: 'A',foo: f}
}
}
const a = new A()
const fn = a.foo
fn() // 虽然是直接调用,但是箭头函数的this不受运行时影响
- 与之类似,将该方法作为回调函数进行调用
class A {
name = 'A'
foo = () => {
console.log('arrow method call')
// this 是当前类的实例,即使是直接调用,它也是类的实例。因为箭头函数的this在定义时确定,而非运行时
console.log(this) // A {name: 'A',foo: f}
}
}
class B {
name = 'B'
bar(f) {
console.log('bar method call')
console.log(this) // B{name: 'B',bar: f)
f() // 如果是箭头函数,其内部this和B没有关系
}
}
const a = new A()
const b = new B()
b.bar(a.foo)
- 将箭头函数传递到异步事件中
class A {
name = 'A'
foo() {
console.log('foo method call')
console.log(this) // A {name: 'A', foo: f}
setTimeout(() => {
console.log('foo method call')
console.log(this) // A {name: 'A', foo: f}, 获取的是箭头函数在定义时上下文的this的值
}, 1)
}
}
const a = new A()
a.foo()