首先, 依然回顾《JS 基础梳理 - 究竟什么是执行上下文栈(执行栈), 执行上下文(可执行代码)?》中的
3. 执行上下文的生命周期
3.1 创建阶段
生成变量对象(Variable object, VO)
建立作用域链(Scope chain)
确定 this 指向
3.2 执行阶段
变量赋值
函数引用
执行其他代码
我们已经梳理了在执行上下文中的变量对象是如何生成的以及作用域链是如何建立的. 本篇文章就继续梳理下, 执行上下文的 this 指向是如何确定的.
首先, 执行上下文分全局执行上下文和函数执行上下文, 在浏览器环境的全局执行上下文中, this 指向全局对象, 也就是 Windows(接下来本篇文章都仅只浏览器环境). 这也相对简单, 没有那么多复杂的情况需要考虑.
而在函数执行上下文中, 确定 this 指向发生在函数执行上下文的创建阶段, 而函数执行上下文又是在函数被调用后才产生的. 因此, 不难理解: this 的指向, 是在函数被调用的时候确定的. 而不是函数声明的时候确定的. 而确定 this 的指向难就难在函数被调用的方式是多种多样的, 所以我们就需要从函数执行的各种方式分别去分析 this 的指向.
1. this 与普通函数执行(默认绑定)
当一个函数执行不带任何修饰时, 使用默认绑定规则.
默认绑定: 函数体如果在非严格模式下, this 绑定到 Windows, 严格模式下绑定到 undefined.
- // 1.1 函数体在非严格模式下的全局函数执行
- function fn () {
- console.log(this)
- }
- fn1() // => Windows
- // 1.2 函数体在严格模式下的全局函数执行
- 'use strict'
- function fn () {
- console.log(this)
- }
- fn() // => undefined
- // 1.3 函数体在非严格模式下的函数中的函数执行
- function fn1 () {
- function fn2 () {
- console.log(this)
- }
- fn2()
- }
- fn1() // => Windows
- // 1.4 函数体在严格模式下的函数中的函数执行
- 'use strict'
- function fn1 () {
- function fn2 () {
- console.log(this)
- }
- fn2()
- }
- fn1() // => undefined
- // 1.5 函数体在非严格模式下, 而函数调用在严格模式下时, this 依然指向 Windows
- function fn () {
- console.log(this)
- }
- (function () {
- 'use strict'
- fn() // => Windows
- })()
2. this 与对象中的方法执行(隐式绑定)
2.1 无论是否是严格模式, 当函数引用有上下文对象时, 隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象.
- // 2.1.1 函数直接在对象中声明
- var obj = {
- a: 1,
- test: function () {
- console.log(this.a)
- }
- }
- obj.test(); // => 1
- // 2.1.2 函数先声明, 再由对象引用
- function test () {
- console.log(this.a)
- }
- var obj = {
- a: 1,
- test: test
- }
- obj.test(); // => 1
2.2 对象属性引用链中只有最顶层或者说只有最后一层会影响调用位置
- // 2.2.1 多层对象引用, this 指向离函数调用最近的对象
- function test () {
- console.log(this.a)
- }
- var obj2 = {
- a: 2,
- test: test
- }
- var obj1 = {
- a: 1,
- obj2: obj2
- }
- obj1.obj2.test() // => 2
2.3 隐式丢失: 被隐式绑定的函数可能会丢失绑定对象.
- // 2.3.1 将 obj.foo 当作函数别名赋值给一个变量
- function foo () {
- console.log(this.a)
- }
- var obj = {
- a: 2,
- foo: foo
- }
- var bar = obj.foo // 函数别名
- var a = '全局属性'
- bar() // => 全局属性
在 2.3.1 中, 虽然 bar 是 obj.foo 的一个引用, 但是实际上, 它引用的是 foo 函数本身, 因此此时的 bar() 其实是一个不带任何修饰的普通函数调用. 因此也使用默认绑定规则.
- // 2.3.2 将 obj.foo 当作 bar 的回调函数.
- function foo () {
- console.log(this.a)
- }
- function bar (fn) {
- fn()
- }
- var obj = {
- a: 2,
- foo: foo
- }
- var a = '全局属性'
- bar(obj.foo) // => 全局属性
大家都知道所有的函数的参数都是按值传递的,(都是栈内数据的拷贝).
基本类型传的是值本身(因为直接把值存在栈内), 引用类型传的是对象在内存里面的地址(因为复杂对象在堆内, 所以在栈里存对象所在的堆地址).
因此 bar(obj.foo) 执行时, 参数 fn 实际上引用的是 foo. 而 foo 函数执行其实就是一个不带任何修饰的普通函数调用. 所以它也使用默认绑定规则.
2.3.4
由此可扩展到 setInterval, setTimeout, 以及匿名函数中的 this 也是使用的默认绑定规则. 即非严格模式下, this 指向 Windows, 严格模式下, this 指向 undefined.
3.this 与 call,apply,bind(显示绑定)
显示绑定规则: this 指向第一个参数.
- 3.1 call
- // 3.1.1
- var xw = {
- name : "小王",
- gender : "男",
- age : 24,
- say : function(school,grade) {
- console.log(this.name + "," + this.gender + ", 今年" + this.age + ", 在" + school + "上" + grade);
- }
- }
- var xh = {
- name : "小红",
- gender : "女",
- age : 12
- }
- xw.say.call(xh, "实验小学", "六年级") // => 小红 , 女 , 今年 12 , 在实验小学上六年级
在 3.1.1 代码示例中, 当调用 say 时强制把它的 this 绑定到了 xh 上.
- 3.2 apply
- // 3.2.1
- var xw = {
- name : "小王",
- gender : "男",
- age : 24,
- say : function(school,grade) {
- console.log(this.name + "," + this.gender + ", 今年" + this.age + ", 在" + school + "上" + grade);
- }
- }
- var xh = {
- name : "小红",
- gender : "女",
- age : 12
- }
- xw.say.apply(xh,["实验小学","六年级"]) // => 小红 , 女 , 今年 12 , 在实验小学上六年级
- 3.3 bind
- // 3.3.1
- var xw = {
- name : "小王",
- gender : "男",
- age : 24,
- say : function(school,grade) {
- alert(this.name + "," + this.gender + ", 今年" + this.age + ", 在" + school + "上" + grade);
- }
- }
- var xh = {
- name : "小红",
- gender : "女",
- age : 12
- }
- xw.say.bind(xh)("实验小学","六年级") // => 小红 , 女 , 今年 12 , 在实验小学上六年级
通过以上这些例子, 其实也可以明显的看到 call,apply,bind 的区别.
3.4 显示绑定的变种 - 硬绑定
之前说到隐式丢失的问题, 而显示绑定的一个变种可以解决隐式丢失的问题, 这种方式被称之为硬绑定.
- // 2.3.2 将 obj.foo 当作 bar 的回调函数.
- function foo () {
- console.log(this.a)
- }
- function bar (fn) {
- fn()
- }
- var obj = {
- a: 2,
- foo: foo
- }
- var a = '全局属性'
- bar(obj.foo) // => 全局属性
将其修改成
- // 3.4.1 利用 call 方法解决隐式丢失的问题
- function foo(){
- console.log(this.a);
- }
- function bar(fn){
- fn.call(obj);
- }
- var obj = {
- a:2,
- foo:foo
- }
- var a = "全局属性";
- bar(obj.foo); // => 2
这里依旧是创建了 bar()这个函数, 但是在其内部手动调用了 obj.foo.call(obj), 把 foo 强制绑定到了 obj 对象, 之后无论如何调用 bar(), 它总会手动在 obj 上调用 foo.
再看看隐式丢失的代码示例 2.3.1
- // 2.3.1 将 obj.foo 当作函数别名赋值给一个变量
- function foo () {
- console.log(this.a)
- }
- var obj = {
- a: 2,
- foo: foo
- }
- var bar = obj.foo // 函数别名
- var a = '全局属性'
- bar() // => 全局属性
将其修改成
- function foo(){
- console.log(this.a);
- }
- var obj = {
- a:2,
- foo:foo
- };
- var bar = obj.foo.bind(obj);
- var a = "global";
- bar(); // => 2
总结下 "显示绑定三人组"
共同点:
1, 都用于控制 this 指向;
2, 第一个参数都是 this 需要指向的对象, 也就是上下文;
3, 都可以后续参数传递;
4, 没有任何参数时或者第一个参数是 null 时, this 都指向全局对象 Windows>
区别:
1,call 后面的参数与 say 方法中的参数是一一对应的, 而 apply 的第二个参数是一个数组, 数组中的元素是和 say 方法中一一对应的. 所以当传入的参数数目不确定时, 多使用 apply.
2,call,apply 绑定后立刻执行, bind 是延迟执行. 换言之, 当你希望改变上下文环境之后并非立即执行, 而是回调执行的时候, 就使用 bind()方法吧
扩散性思考:
call,apply,bind 还有什么实际使用场景? 它们的原理是什么? 如何自己模拟实现这三个方法?
4. this 与构造函数调用(new 绑定)
使用 new 来调用函数, 或者说发生构造函数调用时, 会自动执行下面的操作:
创建 (或者说构造) 一个全新的对象
这个新对象会被执行 [[原型]] 链接.
这个新对象会绑定到函数调用的 this.
如果函数没有返回其他对象, 那么 new 表达式中的函数调用会自动返回这个新对象.
在这里主要关心第 1,3,4 步.
- function Foo(a) {
- this.a = a;
- }
- var bar = new Foo(2);
- console.log(bar.a) // 2
使用 new 来调用 Foo(...)时, 会构造一个新对象并把它绑定到 foo(...)调用中的 this 上, new 是最后一种可以影响到函数调用时 this 绑定行为的方法, 称之为 new 绑定.
5.this 与箭头函数
5.1 箭头函数中的 this 指向包裹它的第一个普通函数中的 this
- // 5.1 箭头函数外部是普通函数调用时
- function fn () {
- return () => {
- return () => {
- console.log(this)
- }
- }
- }
- fn()()() // => Windows
在这个例子中, 因为包裹箭头函数的第一个普通函数是 fn, 而 fn 的 this 指向 Windows, 所以箭头函数中的 this 也是 Windows.
以上的例子, 多数为单个规则.
这里推荐一种方法:
先看看它定义时, 是不是在箭头函数内. 如果是, 那么 this 就指向包裹它的第一个普通函数中的 this.
不管它是不是箭头函数, 我们都需要继续寻找它的 this 指向, 再看它的调用方式. 如果它是被 new 调用, 那么 this 指向实例.
如果调用的时候看到了 bind,call,apply. 那么大部分时候 this 都指向第一个参数.
如果调用时既没有看到 new, 也没有看到 bind,call,apply, 那么大部分时候 this 指向 Windows(非严格模式下).
在写这篇文章的时候, 本来标题是关于 this 指向的全面分析, 后来想想, 其实也并不全面. 真正的宗师级别, 会是多个规则夹在一起去分析 this, 那样的话, 篇幅肯定太长了, 而且一时半会其实也记不了那么多. 而且就这篇文章所讲的, 其实也够大家去理解分析大部分基础的 this 指向问题了. 有兴趣的话, 可以自己去翻阅一下《你不知道 javascritp. 上卷》这本书, 里面还有一些我个人觉得比较少见而没有列出来的 this 指向问题, 比如我的文章中提到了硬绑定, 那其实也会有软绑定.
在这里也贴一篇蚂蚁金服前端的博文, 是一个多个规则综合应用判断 this 指向的题. 很多时候, 当我们以为自己懂了, 而总是会有人更深入的去挖掘我们之前没有想到的知识点, 学无止境.
从这两套题, 重新认识 JS 的 this, 作用域, 闭包, 对象 https://juejin.im/post/59aa71d56fb9a0248d24fae3
来源: https://www.cnblogs.com/hezhi/p/10127575.html