ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

JavaScript this 关键词

2022-06-06 12:04:11  阅读:189  来源: 互联网

标签:JavaScript obj 函数 关键词 绑定 打印 call foo


当一个函数调用时,会创建一个执行上下文,这个上下文包括函数调用的一些信息(调用栈,传入参数,调用方式),this就指向这个执行上下文。

 

this不是静态的,也并不是在编写的时候绑定的,而是在运行时绑定的。它的绑定和函数声明的位置没有关系,只取决于函数调用的方式。

 

this的绑定规则一共有五种:

1、默认绑定

2、隐式绑定

3、显示绑定

4、new绑定

5、ES6新增箭头函数绑定

 

默认绑定

默认绑定通常是指函数独立调用,不涉及其他绑定规则。在严格模式和非严格模式下,this的指向是不同的

1、非严格模式

 

非严格模式,print()独立调用即为默认绑定,this指向window,所以打印this就是打印window。

其实定义的foo和print方法都是在window下的,this指向的是window,所以为this.foo赋值后,之前定义的foo就会变为234

2、严格模式

 

 

 严格模式下,函数内部的this指向undefined

 3、let/const

 

 

 let/const定义的变量存在暂时性死区,而且不会挂载到window对象上,因此print中是无法获取到a和b的。

4、对象内执行

 

 

 foo虽然在objbar函数中,但foo函数仍然是独立运行的,foo中的this依旧指向window对象。

5、方法内执行

var a = 1
function outer () {
  var a = 2
  function inner () { 
    console.log(this.a) // 1
    console.log(this)
  }
  inner()
}
outer()

同4

6、自执行函数

 

 

 默认情况下,自执行函数的this指向window

隐式绑定

1、隐式绑定

 

 

 

  • foo(): 默认绑定,打印1和window
  • obj.foo(): 隐式绑定,打印2和obj

obj是通过var定义的,obj会挂载到window之上的,obj.foo()就相当于window.obj.foo(),这也印证了this永远指向最后调用它的那个对象规则。

2、对象链式调用

 

 

 可以看出this指向的是最后调用它的obj2
3、隐式绑定的丢失

隐式绑定可是个调皮的东西,一不小心它就会发生绑定的丢失。一般会有两种常见的丢失:

  • 使用另一个变量作为函数别名,之后使用别名执行函数
  • 将函数作为参数传递时会被隐式赋值

隐式绑定丢失之后,this的指向会启用默认绑定。

取函数别名

 

JavaScript对于引用类型,其地址指针存放在栈内存中,真正的本体是存放在堆内存中的。

上面将obj.foo赋值给foo,就是将foo也指向了obj.foo所指向的堆内存,此后再执行foo,相当于直接执行的堆内存的函数,与obj无关,foo为默认绑定。笼统的记,只要fn前面什么都没有,肯定不是隐式绑定


函数作为参数传递

 

 

 

 用函数预编译的知识来解答这个问题:函数预编译四部曲前两步分别是:

  1. 找形参和变量声明,值赋予undefined
  2. 将形参与实参相统一,也就是将实参的值赋予形参。

obj.foo作为实参,在预编译时将其值赋值给形参fn,是将obj.foo指向的地址赋给了fn,此后fn执行不会与obj产生任何关系。fn为默认绑定。

 

回调函数

var name='zcxiaobao';
function introduce(){
    console.log('Hello,My name is ', this.name);
}
const Tom = {
    name: 'TOM',
    introduce: function(){
        setTimeout(function(){
            console.log(this)
            console.log('Hello, My name is ',this.name);
        })
    }
}
const Mary = {
    name: 'Mary',
    introduce
}
const Lisa = {
    name: 'Lisa',
    introduce
}

Tom.introduce();
setTimeout(Mary.introduce, 100);
setTimeout(function(){
    Lisa.introduce();
},200);

 

Tom.introduce()执行: console位于setTimeout的回调函数中,是独立运行的,所以此时this指向window
Mary.introduce直接作为setTimeout的函数参数,实际不受穿入参数影响,会发生上面说的隐式绑定丢失,变为默认绑定
Lisa.introduce执行虽然位于setTimeout的回调函数中,但保持xxx.fn()模式,为隐式绑定,this此时指向xxx,即Lisa

所以如果我们想在setTimeoutsetInterval中使用外界的this,需要提前存储一下,避免this的丢失。

 

 

 

 

显式绑定

 

 

 显式绑定比较好理解,就是通过call()、apply()、bind()等方法,强行改变this指向。

上面的方法虽然都可以改变this指向,但使用起来略有差别:

  • call()和apply()函数会立即执行
  • bind()函数会返回新函数,不会立即执行函数
  • call()和apply()的区别在于call接受若干个参数,apply接受数组。

 

三种调用方式

 

 

 

 

 

 

 

 

  • foo(): 默认绑定。
  • foo.call(obj): 显示绑定,foothis指向obj
  • foo.apply(obj): 同call
  • foo.bind(obj): 显式绑定,但不会立即执行函数,没有返回值

 

通过显式绑定修复隐式绑定丢失

 

 1、首先修正讲doFoo的this指向obj

2、修正fn的this

 

回调函数与call

 

 

 注意call的位置1

  • foo(): 默认绑定
  • foo.call(obj): 显式绑定
  • foo().call(obj): 对foo()执行的返回值执行callfoo返回值为undefined,执行call()会报错

 注意call的位置2

 

 

  • foo(): 默认绑定
  • foo.call(obj): 显式绑定
  • foo().call(obj): foo()执行,打印2,返回匿名函数通过callthis指向obj,打印1

这里千万注意:最后一个foo().call(obj)有两个函数执行,会打印2个值

bind

call会立即执行函数,而bind会返回一个新函数,但不会执行

 

 

首先我们要先确定,最后会输出几个值?bind不会执行函数,因此只有两个foo()会打印a

  • foo(): 默认绑定,打印2
  • foo.bind(obj): 返回新函数,不会执行函数,无输出
  • foo().bind(obj): 第一层foo(),默认绑定,打印2,后bindfoo()返回的匿名函数this指向obj,不执行

 

外层this与内层this
如果使用call、bind等修改了外层函数的this,那内层函数的this会受影响吗?

 

 foo.call(obj): 第一层函数foo通过callthis指向obj,打印1;第二层函数为匿名函数,默认绑定,打印2

 

对象中的call

 

  • obj.foo()(): 第一层obj.foo()执行为隐式绑定,打印出foo:obj;第二层匿名函数为默认绑定,打印inner:window
  • obj.foo.call(obj2)(): 类似题目4.7,第一层obj.foo.call(obj2)使用callobj.foothis指向obj2,打印foo: obj2;第二层匿名函数默认绑定,打印inner:window
  • obj.foo().call(obj2): 类似题目4.5,第一层隐式绑定,打印:foo: obj,第二层匿名函数使用callthis指向obj2,打印inner: obj2

 

带参数的call

 

要注意call执行的位置:

  • obj.foo(a).call(obj2, 1):
    • obj.foo(a): foo的AO中b值为传入的a(形参与实参相统一),值为2,返回匿名函数fn
    • 匿名函数fn.call(obj2, 1): fn的this指向为obj2,c值为1
    • this.a + b + c = obj2.a + FooAO.b + c = 3 + 2 + 1 = 6
  • obj.foo.call(obj2)(1):
    • obj.foo.call(obj2): obj.foo的this指向obj2,未传入参数,b = this.a = obj2.a = 3;返回匿名函数fn
    • 匿名函数fn(1): c = 1,默认绑定,this指向window
    • this.a + b + c = window.a + obj2.a + c = 2 + 3 + 1 = 6

 

显式绑定扩展

上面提了很多call/apply可以改变this指向,但都没有太多实用性。下面来一起学几个常用的call与apply使用。

 

apply求数组最值

 

JavaScript中没有给数组提供类似max和min函数,只提供了Math.max/min,用于求多个数的最值,所以可以借助apply方法,直接传递数组给Math.max/min

 

 

 

类数组转为数组

 

 ES6未发布之前,没有Array.from方法可以将类数组转为数组,采用Array.prototype.slice.call(arguments)[].slice.call(arguments)将类数组转化为数组。

es6 的Array.from

 

 

数组高阶函数

日常编码中,我们会经常用到forEach、map等,但这些数组高阶方法,它们还有第二个参数thisArg,每一个回调函数都是显式绑定在thisArg上的。

例如下面这个例子

 

这个例子里的function显示绑定在第二个参数,即obj上

 

new绑定

使用new来构建函数,会执行如下四部操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 为新创建的空对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 为新创建的空对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this
通过new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this。

new绑定

 属性加方法

 

 

  • zc.introduce(): zc是new创建的实例,this指向zc,打印zc
  • zc.howOld()(): zc.howOld()返回一个匿名函数,匿名函数为默认绑定,因此打印18

 

new界的天王山

 

 分析后面三个打印结果之前,先补充一些运算符优先级方面的知识

 

从上图可以看到,部分优先级如下:new(带参数列表) = 成员访问 = 函数调用 > new(不带参数列表)

 

new Foo.getName()

 

首先从左往右看:new Foo属于不带参数列表的new(优先级19),Foo.getName属于成员访问(优先级20),getName()属于函数调用(优先级20),同样优先级遵循从左往右执行。

  • Foo.getName执行,获取到Foo上的getName属性
  • 此时原表达式变为new (Foo.getName)()new (Foo.getName)()为带参数列表(优先级20),(Foo.getName)()属于函数调用(优先级20),从左往右执行
  • new (Foo.getName)()执行,打印2,并返回一个以Foo.getName()为构造函数的实例

这里有一个误区:很多人认为这里的new是没做任何操作的的,执行的是函数调用。那么如果执行的是Foo.getName(),调用返回值为undefinednew undefined会发生报错,并且我们可以验证一下该表达式的返回结果。

 

 可见在成员访问之后,执行的是带参数列表格式的new操作。

 

new Foo().getName()

  • 同上一样分析,先执行new Foo(),返回一个以Foo为构造函数的实例
  • Foo的实例对象上没有getName方法,沿原型链查找到Foo.prototype.getName方法,打印3

new new Foo().getName()

从左往右分析: 第一个new不带参数列表(优先级19),new Foo()带参数列表(优先级20),剩下的成员访问和函数调用优先级都是20

  • new Foo()执行,返回一个以Foo为构造函数的实例
  • 在执行成员访问,Foo实例对象在Foo.prototype查找到getName属性
  • 执行new (new Foo().getName)(),返回一个以 Foo.prototype.getName()为构造函数的实例,打印3

 

 

箭头函数

箭头函数没有自己的this,它的this指向外层作用域的this,且指向函数定义时的this而非执行时。

  1. this指向外层作用域的this: 箭头函数没有this绑定,但它可以通过作用域链查到外层作用域的this
  2. 指向函数定义时的this而非执行时: JavaScript是静态作用域,就是函数定义之后,作用域就定死了,跟它执行时的地方无关。更详细的介绍见JavaScript之静态作用域与动态作用域

对象方法使用箭头函数

 

 上文说到,箭头函数的this通过作用域链查到,intro函数的上层作用域为window

 

箭头函数与普通函数比较

 

 

  • obj.intro2()(): 不做赘述,打印My name is tom
  • obj.intro()(): obj.intro()返回箭头函数,箭头函数的this取决于它的外层作用域,因此箭头函数的this指向obj,打印My name is zc

箭头函数与普通函数的嵌套

  • obj1.intro()(): 类似题目7.2,打印obj1,obj1
  • obj2.intro()(): obj2.intro()为箭头函数,this为外层作用域this,指向window。返回匿名函数为默认绑定。打印window,window
  • obj3.intro()(): obj3.intro()obj2.intro()相同,返回值为箭头函数,外层作用域introthis指向window,打印window,window

new碰上箭头函数


  • zcnew User实例,因此构造函数Userthis指向zc
  • zc.intro(): 打印My name is zc
  • zc.howOld(): howOld为箭头函数,箭头函数this由外层作用域决定,且指向函数定义时的this,外层作用域为Userthis指向zc,打印My age is 24

 

call碰上箭头函数

箭头函数由于没有this,不能通过call\apply\bind来修改this指向,但可以通过修改外层作用域的this来达成间接修改

  • obj1.intro.call(obj2)(): 第一层函数为普通函数,通过call修改thisobj2,打印obj2。第二层函数为箭头函数,它的this与外层this相同,同样打印obj2
  • obj1.intro().call(obj2): 第一层函数打印obj1,第二次函数为箭头函数,call无效,它的this与外层this相同,打印obj1
  • obj1.intro2.call(obj2)(): 第一层为箭头函数,call无效,外层作用域为window,打印window;第二次为普通匿名函数,默认绑定,打印window
  • obj1.intro2().call(obj2): 与上同,打印window;第二层为匿名函数,call修改thisobj2,打印obj2

箭头函数扩展

总结

  • 箭头函数没有this,它的this是通过作用域链查到外层作用域的this,且指向函数定义时的this而非执行时。
  • 不可以用作构造函数,不能使用new命令,否则会报错
  • 箭头函数没有arguments对象,如果要用,使用rest参数代替
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数。
  • 不能用call/apply/bind修改this指向,但可以通过修改外层作用域的this来间接修改。
  • 箭头函数没有prototype属性。

避免使用场景

箭头函数定义对象方法

 

箭头函数不能作为构造函数

 

综合题

对象综合体

 

 

隐式绑定丢失

  • foo.bar(): 隐式绑定,打印20
  • (foo.bar)(): 上面提到过运算符优先级的知识,成员访问与函数调用优先级相同,默认从左到右,因此括号可有可无,隐式绑定,打印20
  • (foo.bar = foo.bar)():隐式绑定丢失,给foo.bar起别名,虽然名字没变,但是foo.bar上已经跟foo无关了,默认绑定,打印10
  • (foo.bar, foo.bar)(): 隐式绑定丢失,起函数别名,将逗号表达式的值(第二个foo.bar)赋值给新变量,之后执行新变量所指向的函数,默认绑定,打印10

上面那说法有可能有几分难理解,隐式绑定有个定性条件,就是要满足XXX.fn()格式,如果破坏了这种格式,一般隐式绑定都会丢失。

arguments

这个题要注意一下,有坑。

  • fn(): 默认绑定,打印10

  • arguments[0](): 这种执行方式看起来就怪怪的,咱们把它展开来看看:

    1. arguments是一个类数组,arguments展开,应该是下面这样:
    arguments: {
        0: fn,
        1: 1,
        length: 2
    }
    
    1. arguments[0]: 这是访问对象的属性0?0不好理解,咱们把它稍微一换,方便一下理解:
    arguments: {
        fn: fn,
        1: 1,
        length: 2
    }
    
    1. 到这里大家应该就懂了,隐式绑定,fn函数this指向arguments,打印2

压轴题

fn.call(null) 或者 fn.call(undefined) 都相当于fn()

  1. obj.fn为立即执行函数: 默认绑定,this指向window

    我们来一句一句的分析:

    • var number: 立即执行函数的AO中添加number属性,值为undefined
    • this.number *= 2: window.number = 10
    • number = number * 2: 立即执行函数AOnumber值为undefined,赋值后为NaN
    • number = 3: AOnumber值由NaN修改为3
    • 返回匿名函数,形成闭包

    此时的obj可以类似的看成以下代码(注意存在闭包):

    obj = {
       number: 3,
       fn: function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    }
    复制代码
  2. myFun.call(null): 相当于myFun(),隐式绑定丢失,myFunthis指向window

    依旧一句一句的分析:

    • var num = this.number: this指向windownum = window.num = 10
    • this.number *= 2: window.number = 20
    • console.log(num): 打印10
    • number *= 3: 当前AO中没有number属性,沿作用域链可在立即执行函数的AO中查到number属性,修改其值为9
    • console.log(number): 打印立即执行函数AO中的number,打印9
  3. obj.fn(): 隐式绑定,fnthis指向obj

    继续一步一步的分析:

    • var num = this.number: this->objnum = obj.num = 3
    • this.number *= 2: obj.number *= 2 = 6
    • console.log(num): 打印num值,打印3
    • number *= 3: 当前AO中不存在number,继续修改立即执行函数AO中的numbernumber *= 3 = 27
    • console.log(number): 打印27
  4. console.log(window.number): 打印20

这里解释一下,为什么myFun.call(null)执行时,找不到number变量,是去找立即执行函数AO中的number,而不是找window.number: JavaScript采用的静态作用域,当定义函数后,作用域链就已经定死。(更详细的解释文章最开始的推荐中有)

总结

  • 默认绑定: 非严格模式下this指向全局对象,严格模式下this会绑定到undefined
  • 隐式绑定: 满足XXX.fn()格式,fnthis指向XXX。如果存在链式调用,this永远指向最后调用它的那个对象
  • 隐式绑定丢失:起函数别名,通过别名运行;函数作为参数会造成隐式绑定丢失。
  • 显示绑定: 通过call/apply/bind修改this指向
  • new绑定: 通过new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this
  • 箭头函数绑定: 箭头函数没有this,它的this是通过作用域链查到外层作用域的this,且指向函数定义时的this而非执行时

 

转载于https://juejin.cn/post/7019470820057546766

 

标签:JavaScript,obj,函数,关键词,绑定,打印,call,foo
来源: https://www.cnblogs.com/-xiao-wu-ge/p/16193920.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有