前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【知乎热门回答】——JavaScript 的 this 原理是什么?

【知乎热门回答】——JavaScript 的 this 原理是什么?

作者头像
童欧巴
发布2021-08-20 17:25:03
5340
发布2021-08-20 17:25:03
举报
文章被收录于专栏:前端食堂前端食堂

知乎上「JavaScript 的 this 原理是什么?」这个问题下:有一句话解释 JavaScript 的 this 指向的,有抖机灵“骗赞”的;有人说 JavaScript 的 this 某种程度上体现了 JavaScript 初期设计的不足,不需要太过于研究这些糟粕;也有的翻出规范,“照本宣科”,也许这会让你更“云里雾里”。

其实,不管 JavaScript 的 this 它好也罢,坏也罢,我觉得了解其指向还是非常重要的,至少能让你少走弯路,少写 bug,也能让你了解这门“奇怪”的语言,从而驾驭它,征服它。

我“荼毒”于应式教育,“笃信“某些概念“只有记死,才能用活”。如果你有时间,有兴趣,那就让我们从例子中体会,死记硬背它,我相信你慢慢会完全理解的。

最后啰嗦一句:如果你被 JavaScript 这些难缠的问题折磨不轻,请不要灰心,不信你翻到答案最后看看我的观点?

场景1:全局环境下的 this

这种情况相对简单直接,函数在浏览器全局环境中被简单调用,非严格模式下this指向window; 在use strict指明严格模式的情况下就是undefined:

代码语言:javascript
复制
function f1 () {
    console.log(this)
}
function f2 () {
    'use strict'
    console.log(this)
}
f1() // window
f2() // undefined

这样的题目比较基础,但是如果你是在面试,那么需要候选人格外注意其变种(为什么总有这种变态无聊面试题),请再看:

代码语言:javascript
复制
const foo = {
    bar: 10,
    fn: function() {
       console.log(this)
       console.log(this.bar)
    }
}
var fn1 = foo.fn
fn1()

这里this仍然指向的是window。虽然fn函数在foo对象中作为方法被引用,但是在赋值给fn1之后,fn1的执行仍然是在window的全局环境中。因此输出windowundefined,还是上面这道题目,如果调用改变为:

代码语言:javascript
复制
const foo = {
    bar: 10,
    fn: function() {
       console.log(this)
       console.log(this.bar)
    }
}
foo.fn()

将会输出:

代码语言:javascript
复制
{bar: 10, fn: ƒ}
10

这其实属于第二种情况了,因为这个时候this指向的是最后调用它的对象,在foo.fn()语句中this指向foo对象。请记住:在执行函数时,如果函数中的this是被上一级的对象所调用,那么this指向的就是上一级的对象;否则指向全局环境。

场景2:上下文对象调用中的 this

我们直接来看“难”一点的:当存在更复杂的调用关系时,

代码语言:javascript
复制
const person = {
    name: 'Lucas',
    brother: {
        name: 'Mike',
        fn: function() {
            return this.name
        }
    }
}
console.log(person.brother.fn())

在这种嵌套的关系中,this指向最后调用它的对象,因此输出将会是:Mike

我们再看一道更复杂的题目,请跟我一起做好“应试”的准备:

代码语言:javascript
复制
const o1 = {
    text: 'o1',
    fn: function() {
        return this.text
    }
}
const o2 = {
    text: 'o2',
    fn: function() {
        return o1.fn()
    }
}
const o3 = {
    text: 'o3',
    fn: function() {
        var fn = o1.fn
        return fn()
    }
}

console.log(o1.fn())
console.log(o2.fn())
console.log(o3.fn())

答案是:o1o1undefined,你答对了吗?

我们来一一分析。

  • 第一个 console 最简单,o1 没有问题。难点在第二个和第三个上面,关键还是看调用 this 的那个函数。
  • 第二个 consoleo2.fn(),最终还是调用 o1.fn(),因此答案仍然是 o1
  • 最后一个,在进行 var fn = o1.fn 赋值之后,是“裸奔”调用,因此这里的 this 指向 window,答案当然是 undefined

如果是在面试中,我作为面试官,就会追问:如果我们需要让:

代码语言:javascript
复制
console.log(o2.fn())

输出 o2,该怎么做?

一般开发者可能会想到使用bind/call/apply来对this的指向进行干预,这确实是一种思路。但是我接着问,如果不能使用bind/call/apply,有别的方法吗?

代码语言:javascript
复制
const o1 = {
    text: 'o1',
    fn: function() {
        return this.text
    }
}
const o2 = {
    text: 'o2',
    fn: o1.fn
}

console.log(o2.fn())

还是应用那个重要的结论:this 指向最后调用它的对象,在 fn 执行时,挂到 o2 对象上即可,我们提前进行了类似赋值的操作。

场景3:bind/call/apply 改变 this 指向

上文提到 bind/call/apply:

代码语言:javascript
复制
const foo = {
    name: 'lucas',
    logName: function() {
        console.log(this.name)
    }
}
const bar = {
    name: 'mike'
}
console.log(foo.logName.call(bar))

将会输出mike,这不难理解。但是对 call/apply/bind 的高级考察往往会结合构造函数以及组合式实现继承。实现继承的话题,我们会单独讲到。构造函数的使用案例,我们结合下面的场景进行分析。

场景 4:构造函数和 this

这方面最直接的例题为:

代码语言:javascript
复制
function Foo() {
    this.bar = "Lucas"
}
const instance = new Foo()
console.log(instance.bar)

答案将会输出 Lucas。但是这样的场景往往伴随着下一个问题:new 操作符调用构造函数,具体做了什么?以下供参考:

  • 创建一个新的对象;
  • 将构造函数的 this 指向这个新对象;
  • 为这个对象添加属性、方法等;
  • 最终返回新对象。

以上过程,也可以用代码表述:

代码语言:javascript
复制
var obj  = {}
obj.__proto__ = Foo.prototype
Foo.call(obj)

当然,这里对 new 的模拟是一个简单基本版的,更复杂的情况这个问题下我不会赘述。

需要指出的是,如果在构造函数中出现了显式 return 的情况,那么需要注意分为两种场景:

代码语言:javascript
复制
function Foo(){
    this.user = "Lucas"
    const o = {}
    return o
}
const instance = new Foo()
console.log(instance.user)

将会输出 undefined,此时 instance 是返回的空对象 o

代码语言:javascript
复制
function Foo(){
    this.user = "Lucas"
    return 1
}
const instance = new Foo()
console.log(instance.user)

将会输出 Lucas,也就是说此时 instance 是返回的目标对象实例 this

结论:如果构造函数中显式返回一个值,且返回的是一个对象,那么 this 就指向这个返回的对象;如果返回的不是一个对象,那么 this 仍然指向实例。

场景 5:箭头函数中的 this 指向

箭头函数使用this不适用以上标准规则,而是根据外层(函数或者全局)上下文作用域来决定。

来看题目:

代码语言:javascript
复制
const foo = {  
    fn: function () {  
        setTimeout(function() {  
            console.log(this)
        })
    }  
}  
console.log(foo.fn())

这道题中,this 出现在 setTimeout() 中的匿名函数里,因此 this 指向 window 对象。如果需要 this 指向 foo 这个 object 对象,可以巧用箭头函数解决:

代码语言:javascript
复制
const foo = {  
    fn: function () {  
        setTimeout(() => {  
            console.log(this)
        })
    }  
} 
console.log(foo.fn())

// {fn: ƒ}

单纯箭头函数中的 this 非常简单,但是综合所有情况,结合 this 的优先级考察,这时候 this 指向并不好确定。请继续阅读。

终极场景 6:this 优先级相关

我们常常把通过 callapplybindnewthis 绑定的情况称为显式绑定;根据调用关系确定的 this 指向称为隐式绑定。

那么显式绑定和隐式绑定谁的优先级更高呢?

请看例题:

代码语言:javascript
复制
function foo (a) {
    console.log(this.a)
}

const obj1 = {
    a: 1,
    foo: foo
}

const obj2 = {
    a: 2,
    foo: foo
}

obj1.foo.call(obj2)
obj2.foo.call(obj1)

输出分别为 2、1,也就是说 callapply 的显式绑定一般来说优先级比隐式绑定更高。

代码语言:javascript
复制
function foo (a) {
    this.a = a
}

const obj1 = {}

var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a)

上述代码通过 bind,将 bar 函数中的 this 绑定为 obj1 对象。执行 bar(2) 后,obj1.a 值为 2。即经过 bar(2) 执行后,obj1 对象为:{a: 2}

当再使用 bar 作为构造函数时:

代码语言:javascript
复制
var baz = new bar(3)
console.log(baz.a)

将会输出 3。我们看 bar 函数本身是通过 bind 方法构造的函数,其内部已经对将 this 绑定为 obj1,它再作为构造函数,通过 new 调用时,返回的实例已经与 obj1 解绑。也就是说:

new 绑定修改了 bind 绑定中的 this,因此 new 绑定的优先级比显式 bind 绑定更高。

我们再看:

代码语言:javascript
复制
function foo() {
    return a => {
        console.log(this.a)
    };
}

const obj1 = {
    a: 2
}

const obj2 = {
    a: 3
}

const bar = foo.call(obj1)
console.log(bar.call(obj2))

将会输出 2。由于 foo()this 绑定到 obj1bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。

如果将 foo 完全写成箭头函数的形式:

代码语言:javascript
复制
var a = 123
const foo = () => a => {
    console.log(this.a)
}

const obj1 = {
    a: 2
}

const obj2 = {
    a: 3
}

var bar = foo.call(obj1)
console.log(bar.call(obj2))

将会输出 123

这里我再“抖个机灵”,仅仅将上述代码的第一处变量 a 的赋值改为:

代码语言:javascript
复制
const a = 123
const foo = () => a => {
    console.log(this.a)
}

const obj1 = {
    a: 2
}

const obj2 = {
    a: 3
}

var bar = foo.call(obj1)
console.log(bar.call(obj2))

答案将会输出为 undefined,原因是因为使用 const 声明的变量不会挂载到 window 全局对象当中。因此 this 指向 window 时,自然也找不到 a 变量了。关于 const 或者 let 等声明变量的方式不再本课的主题当中,我们后续也将专门进行介绍。


到这里,是否有“融会贯通”的感觉了呢?如果还有困惑,也不要灰心。JavaScript 的 this,需要反复学习,“死记硬背”后才能慢慢体会。至少作为“资深工程师”的我,也不敢保证所有场景都能答对,理解不到位,再找到规范回炉了解一下怕什么呢?

我宽慰自己:也许区别资深和菜鸟工程师的差别,不完全在于回答应试题目的准确率,更在于你怎么思考问题,解决问题吧。你不懂 this 指向,那就动手观察一下,不了解原理,翻出来规范看一下,没有什么大不了~

Happy coding!

公众号:前端食堂

知乎:童欧巴

掘金:童欧巴

这是一个终身学习的男人,他在坚持自己热爱的事情,欢迎你加入前端食堂,和这个男人一起开心的变胖~

“如果你觉得读了本文有收获的话可以点个在看让我看到。阅读过程中有任何问题、想法或者感触也欢迎你在下方留言,也可以在后台回复加群进入食堂的交流群。 沟通创造价值,分享带来快乐。也欢迎你分享给身边有需要的同学,利他就是最好的利己。 ”

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-08-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端食堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 场景1:全局环境下的 this
  • 场景2:上下文对象调用中的 this
  • 场景3:bind/call/apply 改变 this 指向
  • 场景 4:构造函数和 this
  • 场景 5:箭头函数中的 this 指向
  • 终极场景 6:this 优先级相关
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档