脉络探索
-
关于this的所有核心内容,在这一章节都会详细的进行讲解
-
this主要的规则有四个:默认、隐式、显式、new
-
我们本章节所有的内容都是围绕这四种规则进行展开
-
-
学习this的关键点在于如何区分当前情况属于哪一种规则
-
学习了规则,进行常见的情况分析,是我们本章节需要做的事情
-
而更多边缘的情况,都是应用上,浮于表面的内容。掌握了核心的规则,剩下的边界拓展就不会有难度
-
一、什么是this
-
this是在函数执行时用来指代那个“当前执行上下文”的对象
-
也就能说明
this
的值是在函数被调用时确定的,而不是在函数被定义时确定的 -
因为只有当函数执行的时候,才会创建对应的FEC(函数执行上下文)
-
1.1. 为什么需要this
-
在常见的编程语言中,几乎都有this这个关键字(Objective-C中使用的是self),但是JavaScript中的this和常见的面向对象语言中的this不太一样:
-
常见面向对象的编程中,比如Java、C++、Swift、Dart等等一系列语言中,this通常只会出现在类的方法中
-
也就是你需要一个类,类中的方法(特别是实例方法)中,this代表的是当前调用对象
-
但是JavaScript中的this更加灵活,无论是它出现的位置还是它代表的含义
-
-
我们来编写一个obj的对象,看有this跟没有this的区别
1.1.1. this案例
var obj = {
name:"小余",
eacting:function(){
console.log(this.name + "在吃东西");
},
runing:function(){
console.log(this.name + "在跑步");
},
studying:function(){
console.log(this.name + "在学习");
}
}
obj.eacting()
obj.runing()
obj.studying()
//小余在吃东西
//小余在跑步
//小余在学习
1.1.2. 不使用this的弊端
-
不使用this,一样是可以打印出来的,从某些角度来说,开发中没有this,很多问题我们也是有解决方案的
-
但是如果使用this的话,我们就不需要修改对象内部的代码了
-
var obj = {
name:"小余",
eacting:function(){
console.log(obj.name + "在吃东西");
},
runing:function(){
console.log(obj.name + "在跑步");
},
studying:function(){
console.log(obj.name + "在学习");
}
}
obj.eacting()
obj.runing()
obj.studying()
//小余在吃东西
//小余在跑步
//小余在学习
-
这句话怎么理解呢?
-
我们如果再创建一个对象,那此时对象名称就不同了
-
将方法中的对象引用(如
obj.name
和foo.name
)替换为this.name
,那这些方法就变得与特定对象的引用无关,不需要为每个对象编写独立的方法,只需编写一次方法,然后在各个对象间共享。官方点的说法是可以增加代码的可维护性和可扩展性 -
但朴实一点的理由还是可以让我们少写代码,结构更简洁一点。但从而也会造成难度的一丢丢提升
-
var obj = {
name:"小余",
eacting:function(){
console.log(obj.name + "在吃东西");
},
runing:function(){
console.log(obj.name + "在跑步");
},
studying:function(){
console.log(obj.name + "在学习");
}
}
var foo = {
name:"coderwhy",
eacting:function(){
console.log(foo.name + "在吃东西");
},
runing:function(){
console.log(foo.name + "在跑步");
},
studying:function(){
console.log(foo.name + "在学习");
}
}
obj.eacting()
obj.runing()
obj.studying()
-
但是,难度的提升其实并不多。学习之后,只会存在会与不会。会就简单,不会就觉得难
-
在会的基础上,不会觉得难,只会觉得内容麻烦
-
所以this是建立在你会的基础上,去为你节约时间,去掉麻烦,但也会在一开始我们不会的时候,造成门槛难度的提升
-
而这个学习投资在长远来看是值得的
-
1.2. this在全局作用域指向什么
刚刚的this为什么指向于自身函数作用域的上层?如果是初学者的话,可能会有这些疑问
但没有关系,上面只是为了展示this的一个效果,不需要理解,等学完this的绑定规则之后,再回头看,就能够明白
全局中的this是非常特殊的,因为大多数情况下this都是出现在函数中的
-
this在全局作用域下
-
浏览器:window
-
Node环境:{}空对象
-
-
但是,开发中很少直接在全局作用域下去使用this,通常都是在函数中使用
-
this是动态绑定的。动态绑定就是等到我们函数即将执行的时候才会确定绑定上去,而不是解析的时候确定的
-
有着比较多的绑定规则,在不同规则下的绑定情况都不大一样
-
所有的函数在被调用时,都会创建一个执行上下文
-
这个上下文中记录着函数的调用栈、AO对象等
-
this也是其中一条记录
-
//打印出来的是两个一模一样的window(浏览器情况)
console.log(this);
console.log(window);
console.log(this === window);//true
//如果是Node终端输出的话,this是空对象,自然和window也就不相等
1.2.1. node环境下为什么是空对象
文件在要被node执行的时候,我们的文件会被node当作一个模块module -> 加载 ->编译 -> 将所有代码放在一个函数里面 -> 执行这个函数,执行了一个apply
-
function foo(){xxx},执行的时候我们不使用foo(),而是foo.apply("小余"),则"小余"会替代掉xxx的内容,如图8-1
function foo() {
console.log(this);
}
foo.apply("小余");//小余会在foo内部进行打印,foo的this就是传递进去的内容
图8-1 foo.apply("小余")执行效果
-
从图8-2,Node的源码中,可以看到默认的
this
值是指向模块的exports
对象的-
因为在 Node.js 环境中,每个 JavaScript 文件被视为一个模块 当 Node.js 执行一个模块时,它实际上是在内部包装了一层函数
-
这个函数的参数包括
exports
,require
,module
,__filename
, 和__dirname
等,允许模块与其他模块交互或访问其自身的元数据 -
而我们在Node中的this拿到了空对象,只是因为thisValue被赋值为了空对象。所以this所指向的就是空内容了
-
图8-2 Node部分this源码
-
把内容抽象出来,就是如下的部分了,导入导出,模块名,文件名,目录名,然后进行整体操作
(function(exports, require, module, __filename, __dirname) {
// 模块的原始代码
})
1.3 同一个函数this的不同
-
this指向什么,跟函数所处的位置是没有关系的
-
跟函数被调用的方式有关系
我们先来看一个让人困惑的问题:
-
定义一个函数,我们采用三种不同的方式对他进行调用,它产生了三种不同的结果,如图8-3
function foo(){
console.log(this);
}
//1.直接调用这个函数
foo()
//2.创建一个对象,对象中的函数指向foo
var obj = {
name:"小余",
foo:foo
}
obj.foo()
//3.apply调用
foo.apply("XiaoYu")
图8-3 函数的三种调用方式效果
这个的案例可以给我们什么样的启示:
-
函数在调用时,JavaScript会默认给this绑定一个值
-
this的绑定和
定义的位置(编写的位置)没有关系
-
this的绑定和
调用方式以及调用的位置有关系
-
this是在运行时被绑定的
-
这就需要让我们继续完善我们函数调用时的内存图了,如图8-4
-
把我们之前用过的内存图拿过来,在这里进行补充上,在我们的
函数执行上下文
中需要加上this了 -
回到一开始解释什么是this的时候:函数执行时用来指代那个“当前执行上下文”的对象,是不是会有一个更清晰的概念了
-
图8-4 函数调用内存图
二、this的绑定规则
-
this的绑定规则是其最核心的部分
-
内容不多,但由于应用场景的繁多,所以运用方式非常的多
-
但只需要抓住最核心的概念,就不会被轻易迷惑
-
-
只有
显式绑定
可以人为的改变this指向,其他三种绑定规则都是不变的-
而我们现在已经知道this无非就是在函数调用时被绑定的一个对象,我们就需要知道它在不同类型场景下的绑定规则即可
-
2.1. 规则1 默认绑定
2.1.1. 案例1
什么情况下使用默认绑定呢?独立函数调用
-
独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用
-
这种情况下,this指向的就是window
//案例1
//函数在被调用的时候,没有被绑定在任何的对象上面,也没有使用apply等方式调用
function foo(){
console.log(this);
}
foo()
2.1.2. 案例2
-
另外的一种情况:函数也是独立调用的
-
当函数直接被调用,如
foo()
,而不是作为某个对象的方法或通过属性访问时:XXX.foo()
,这种调用被认为是独立的,结果如图8-5
-
//案例2
function foo1(){
console.log("foo1",this);
}
function foo2(){
console.log("foo2",this);
foo1()
}
function foo3(){
console.log("foo3",this);
foo2()
}
foo3()
图8-5 案例2代码结果
2.1.3. 案例3+4
//案例3
var obj = {
name:"小余",
foo:function(){
console.log(this);
}
}
var fn = obj.foo
fn()
//答案是指向window,而不是obj
要注意,案例3是定义的时候有绑定对象上面,但是当他在执行的时候,我们执行的是fn,根据之前学的内存里的执行过程,我们知道fn此时执行的就是obj里面的function本身(内存位置),那fn是独立调用的其实就证明了函数也是独立调用的,答案就应该指向window
this指向什么,跟函数所处的位置是没有关系的,跟调用的位置才有关系
//案例四
function foo(){
console.log(this);
}
var obj = {
name:"小余",
foo:foo
}
var bar = obj.foo
bar()
//window
-
跟案例3只有一点点的小变化,只是将foo移到了obj外面,然后在obj内部进行引用了,本质上没有变化
-
因为调用的位置是一样的
-
这个foo移动到全局中,不会对this造成影响,会造成影响的是作用域链的查找范围,因为foo和obj现在是同级了,而在案例3中,foo是在obj的内部的,则foo可以沿着作用域链(父级作用域)访问到obj里的属性
-
2.1.4. 案例5
之前用过的非常熟悉的案例
-
是我们在学习闭包时候所使用的案例
-
而闭包调用必然指向window这种结论是错误的,换种调用方式(不再是默认绑定方式的话),就会发生改变了
-
对此,我们换一种
隐式绑定
的方式,指向的就不再是window了。而原因我们马上就会讲到
-
//案例5
function foo(){
function bar(){
console.log(this);
}
return bar
}
var fn = foo()
fn()
//这个时候fn函数调用时返回window,非常熟悉的,fn调用的时foo函数里面的bar函数,并没有扯到foo函数上,属于独立调用。
//如果换一种方式进行调用,闭包不会再指向window
var obj = {
name:"coderwhy",
age:fn
}
obj.age()//隐式绑定
//这种调用方式,js引擎会将obj绑定到我们age的函数内部
//看,此时外面又创建了一个obj对象,里面的age指向就是我们刚刚认定的闭包的fn或者说bar函数,调用顺序是age -> fn -> bar,但此时我们再调用age的时候,返回的结果不再是window了,因为我们输出前的那一刻的调用方式已经发生了变化,此时不再是函数独立调用
2.2. 规则2 隐式绑定
-
另外一种比较常见的调用方式是通过某个对象进行调用的
-
也就是它的调用位置中,是通过某个对象发起的函数调用
-
而这也是我们在规则1中的案例5所展示的
隐式绑定规则
,打破了闭包必定指向window的谣言。这说明了,通过对象发起的调用,this已经发生了变化,如下方案例1以及对应的结果图8-6 -
而这种调用方式,我们在之前有进行区分,这种函数调用方式叫做方法
-
//案例1
function foo(){
console.log(this);
}
var obj = {
name:"coderwhy",
foo:foo
}
obj.foo()//隐式绑定
图8-6 隐式绑定案例1效果图
//案例2
var obj = {
name:"小余",
eating:function(){
console.log(this.name + "在吃东西");
},
running:function(){
console.log(this.name + "在跑步");
}
}
obj.eating()
obj.running()
我们通过这种在一开始就演示过的代码中可以看到,obj里面的eacting跟running函数的this是可以指向函数的父级作用域的,也就是obj函数
-
因为我们在调用的时候,是通过
obj.eating
的方式,将obj绑定到eating里面,所以this指向会指到obj上面 -
object对象会被js引擎绑定到fn函数中的this里面
var fn = obj.eating
fn()//解除obj和eating方法的绑定关系
obj.running()//未解除的绑定关系,进行对比
如果我们将obj跟eating的绑定关系解除掉,再调用eating函数的时候,他的this的指向就会出现问题,如图8-7
图8-7 obj与eating绑定关系解除前后对比
-
eating方法
的this指向出现了问题,this.name的结果出不来了,因为我们在调用的时候已经将obj跟eating函数的关系给去除掉了,obj没有绑定到eating里面了,所以就指向不到了obj里面的内容了
//案例3
var obj1 = {
name:"obj1",
foo:function(){
console.log(this);
}
}
var obj2 = {
name:"obj2",
bar:obj1.foo
}
obj2.bar()
//node环境返回结果:{ name: 'obj2', bar: [Function: foo] }
-
通过案例3,我们调用obj2中的bar属性,obj2.bar属性调用obj1中的foo函数,效果如图8-8
-
其实也不算调用,因为我们没有
()
,就不会执行,而是转为存储foo的内存地址(类似0xa00)
-
图8-8 案例3控制台打印结果
-
此时这个this是绑定到了谁身上,我们通过结果可以看到是obj2的身上,首先我们bar是调用到了obj1中的foo函数身上,但是最后我们执行的时候,是通过obj2来进行执行的,所以obj2就被绑定到了foo函数里面去了,所以此时foo函数控制台打印this的结果才会是obj2里的内容
-
而这就是隐式绑定
-
哪个对象发起的方法调用,其方法this就指向于谁
-
在没和显式规则发生冲突的情况下,这是不会变化的
-
2.3. 规则三 显式绑定
-
隐式绑定规则有点偏向于被动,有前提要求
-
只有你通过了对象调用,我才能将this绑到对象上
-
而这就必须在调用的对象内部有一个对函数的引用(比如一个属性)
-
如果没有这样的引用,在进行调用时,会报找不到该函数的错误
-
正是这个引用,间接的将this绑定到了这个对象上
-
通俗的说就是:以上面obj1、obj2的例子来说,我们obj2如果想要调用obj1里的函数的话,我们就得想办法把obj1里的这个函数放到obj2里的属性里面,然后使用obj2对bar进行一个引用,然后我们才能用obj2.bar进行一个调用
-
如果我们不希望在对象内部包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?
-
它们的区别是:第一个参数是相同的,后面的参数
apply为数组,call为参数列表
-
Prototype原型
和所涉及的其他部分是非常重要的内容,我们通过原型才能让所有函数都能使用这些方法,比this更加复杂,在接下来的章节中,我们也会攻克他,这里则暂时忽略 -
JavaScript所有的函数都可以使用call和apply方法(这个和Prototype有关)
-
这两个函数的第一个参数都要求是一个对象,这个对象的作用是什么呢?就是给this准备的
-
在调用这个函数时,会将this绑定到这个传入的对象上
-
-
这就是显式绑定,能通过几个特定的原生API方法,主动的改变this指向
-
我们可以敏锐的察觉到,当this指向进行改变的时候,原本的this绑定模式就发生了改变。这里一定会有些规则的碰撞问题
-
2.3.1. apply、call、bind的使用
在JavaScript的早期版本中,处理this
的指向是一个常见的问题,特别是在回调函数和闭包中。this
的动态性质在对象方法或函数中维持上下文
变得复杂。为了解决这个问题,apply
和call
方法被引入,允许开发者明确指定函数执行时的上下文。随后,bind
方法在较新的ECMAScript 5规范中被添加,提供了一种更灵活的方式来预设函数的this
值和参数
-
而控制函数的this指向,更本质的说法其实是控制
函数的执行上下文
-
this是指向于
当前的执行上下文
,我把当前的执行上下文换成我想要,this指向的内容不就不一样了,这就是这些方法所做的事情
-
2.3.1.1. call函数
函数上面有call方法,当我们使用foo.call()的时候,他也会去帮我们调用函数。JavaScript内部已经帮我们实现了一个call函数了
语法:func.apply(thisArg, [argsArray])
thisArg: 在
func
函数运行时使用的this
值。需要注意的是,使用null
或undefined
在非严格模式下会自动替换为全局对象(浏览器中的window
),而在严格模式下会保持为null
或undefined
argsArray: 一个数组或者类数组对象,里面的数组元素会按顺序作为参数传给
func
函数。如果不提供此参数或其值为null
或undefined
,则调用函数时不会传入参数
function foo(){
console.log("函数被调用了");
}
foo()
foo.call()
//调用的结果是一样的
// 函数被调用了
// 函数被调用了
2.3.1.2. apply函数
跟call函数同理的,函数上同样有apply函数,一样是JavaScript内部替我们实现的
语法:func.call(thisArg, arg1, arg2, ...)
thisArg:
func
函数运行时的this
值。arg1, arg2, ...: 参数列表,这些参数会按顺序直接传递给
func
函数
function foo(){
console.log("函数被调用了");
}
foo()
foo.apply()
// 函数被调用了
// 函数被调用了
2.3.1.3. call与apply函数的区别
它们两者传参的方式不大一样
call是依次传的
apply是以数组的形式传的
call和apply在执行函数时,是可以明确的绑定this,这个绑定规则称之为显式绑定
function sum(num1,num2){
console.log(num1+num2,this)
}
sum.call("call",20,30)//后面的参数是以逗号来做一个分割的,依次挨个传就行
sum.apply("apply",[20,30])//后面的参数是以数组存在的
//50 [String: 'call']
//50 [String: 'apply']
2.3.1.4. 直接调用与call、apply的不同
如果说foo()
直接调用跟call、apply的结果是一样的,为什么不全部使用直接调用呢?
-
首先,它们的this绑定是不一样的
-
foo()直接调用,指向的是全局对象(window)
-
call、apply可以手动指定我们所指向的this是谁,很多时候我们使用这两个方法的目的也就是这个
-
因此根据下方三种调用方式,可以确定this的三种结果如图8-9
-
function foo(){
console.log("函数被调用了",this);
}
var obj = {
name:"coderwhy"
}
foo()
foo.apply("小余")
foo.call(obj)
图8-9 直接调用与apply、call调用的不同
2.3.2. 显式绑定 bind
当我们要重复使用多次绑定的时候,反复调用call或者apply,往里面填写重复的参数的时候,就会显得比较累赘,这个时候我们就可以使用bind函数来替代,bind函数是会返回一个值的,这个时候我们就可以声明一个函数(这个函数也会重新开辟一个堆空间来进行存放的)来接收他,然后直接调用这个声明的函数就可以了
-
bind语法:func.bind(thisArg[, arg1[, arg2[, ...]]])
-
thisArg: 在绑定函数被调用时作为
this
参数传递给目标函数的值。如果使用new
运算符构造绑定函数,则忽略此值arg1, arg2, ...: 预先添加入绑定函数的参数,在任何实际调用时,这些参数会先于其他被传递的参数
-
function foo(){
console.log(this)
}
// foo.call("小余")
// foo.call("小余")
// foo.call("小余")
// foo.call("小余")
// foo.call("小余")
//默认绑定和现实绑定bind冲突
var newFoo = foo.bind("小余")
newFoo()//是不是比每次都写foo.call("小余")方便一些?
然后我们可以看到,我们调用newFoo函数的时候是独立调用的,这个时候应该是指向window才对,但是我们已经用bind将newFoo的指向明确固化到"小余"上面了,这个时候规则就会冲突,显式绑定bind函数的优先级高于默认绑定
function foo(){
console.log(this)
}
// foo.call("小余")
// foo.call("小余")
// foo.call("小余")
// foo.call("小余")
// foo.call("小余")
//默认绑定和现实绑定bind冲突
var newFoo = foo.bind("小余")
var bar = foo
console.log(bar === foo);//true
console.log(newFoo === foo);//false
我们将foo.bind赋值给了newFoo,又声明了bar来接收了foo函数,我们的目的是为了对比它们的this指向问题
-
第一个对比的是bar函数跟foo函数,很明显,是直接赋值的关系,它们是一样的(包括了this也指向一样的地方),返回true
-
第二个对比的是newFoo函数和foo函数,这里它们的不同在于newFoo接收的并不是foo函数本身,唯一的变量是被bind修改了this指向的foo函数。经过对比,它们是不相等的,返回的是false,也证明了一点:bind函数会返回新的内容,但不会修改原本函数的this(他们指向的不是同一块内存空间,而是不相干的两处地方,不然此时foo与newFoo的对比就该返回true了
2.3.3. bind的特别之处
bind
函数和call
和apply
函数都可以用来改变函数的调用对象。但是它们之间有一些微妙的差别。
-
下面是bind和call、apply函数的一些基本区别:
-
bind
函数会创建一个新函数,调用对象被固定为指定的值。而call
和apply
函数则是立即调用函数,并改变调用对象 -
bind
函数可以在调用时指定函数的参数,而call
和apply
函数则需要在调用时传入所有的参数 -
bind
函数返回的是一个新的函数,而call
和apply
函数则是立即执行函数
-
-
bind作为后出的原生API方法,确实是做出了一些更好的改进
2.4. 规则四 new绑定
-
JavaScript中的函数可以当作一个类的构造函数来使用,也就是使用new关键字
-
这个涉及到了ES6之后出现的
类Class
,我们这里就先简单的进行讲解,在后面的篇章中,我们会更详细的去专门讨论
-
-
使用new关键字来调用函数时,大致分为三步:
-
以构造器的prototype属性为原型,创建全新对象
-
将this(可以理解为上句创建的新对象)和调用参数传给构造器,执行
-
如果构造器没有手动返回其他对象,则返回第一步创建的新对象
-
new来调用,会把我们生成的新的对象赋值给这个Person里面内部的的this,如图8-10
准确来说,js 中的构造函数只是使用new 调用的普通函数,它并不是一个类,最终返回的对象也不是一个实例,只是为了便于理解习惯这么说罢了。
function Person(){
console.log(this);
}
Person()//正常调用
new Person()//new调用
图8-10 正常调用与new调用区别
我们通过一个new关键字调用一个函数时(构造器),这个时候this是在调用这个构造器创建出来的对象
-
this = 创建出来的对象
-
这个绑定过程就是new 绑定
//案例2
function Person(name,age){
this.name = name
this.age = age
}
var p1 = new Person("小余",20)
console.log(p1.name,p1.age);
var p2 = new Person("why",18)
console.log(p2.name,p2.age);
//小余 20
//why 18
三、一些函数的this分析
3.1. 内置函数的绑定思考
-
有些时候,我们会调用一些JavaScript的内置函数,或者一些第三方库的内置函数
-
这些内置函数会要求我们传入另外一个函数
-
我们自己并不会显式的调用这些函数,而且JavaScript内部或者第三方库内部会帮助我们执行
-
这些函数中的this又是如何绑定的呢?
-
-
setTimeout、数组的forEach、div的点击
3.2. setTimeout定时器
-
而函数内部是如何进行调用我们传递进去的函数呢?
-
会像这样内部绑定某个内容吗?
-
function XySetTimeout(fn, duration){
fn.call('coderwhy')
}
-
正常的函数会受到this的影响
-
而箭头函数式的写法,是不会受到this,node打印效果如图8-11
-
setTimeout(function(){
console.log("正常的this",this);//window
})
//箭头函数
setTimeout(()=>{
console.log("箭头函数的this",this);//浏览器为window ,Node环境为{}
},2000)
图8-11 node环境下的结果
-
这说明了
setTimeout
的执行并不绑定任何特定的this
上下文到回调函数。它简单地调度回调函数在未来某个时刻执行,而该函数的this
值在非严格模式下默认为全局对象(浏览器中是window
,Node.js 中是global
),在严格模式下为undefined
。-
如果回调是箭头函数,则
this
保留的是箭头函数定义时的上下文
-
3.3. 监听点击
-
当我们点击一个元素的时候,对其绑定了点击事件,那我们在这个事件中使用this会被绑定到哪里?
-
css样式不写,可以自己写一个宽高加背景颜色出来方便点击
-
我们监听点击中的this给到我们的是监听的对象,也就是如图8-12的内容(div的元素对象):
-
图8-12 监听的对象
<div class="box"></div>
<script src="./你的逻辑文件.js"></script>
//1.只能添加一个,如果重复添加,下面那个会把前面的给覆盖掉
const boxDiv = document.querySelector(".box")
boxDiv.onclick = function(){
console.log(this);
}
//2.能够添加多个,实现原理:将所有的函数收集到数组里面,一旦发生点击的时候,我们就遍历数组,对这些函数进行调用,
//然后内部会进行fn.call(boxDiv),实现将this绑定到boxDiv身上
boxDiv.addEventListener('onclick',function(){
console.log(this);
})
boxDiv.addEventListener('onclick',function(){
console.log(this);
})
boxDiv.addEventListener('onclick',function(){
console.log(this);
})
-
this指向了div的元素对象,这说明了这个boxDiv会拿到内部的函数的,然后进行调用,相当于:
boxDiv.onclick()
-
也就是隐式绑定了(只不过内部进行了,没有显式出来),将boxDiv绑定到了onclick上面,所以this会绑定到div元素对象上
3.4. 数组中的绑定
数组由多种情况,forEach、map、filter等等,我们这里就以forEach作为例子来进行
语法:array.forEach(function(currentValue, index, arr), thisArg)
正常情况下是返回window,但是forEach是接收第二个参数,第二个参数可以帮我们绑定对象,也就包括了this的指向位置
//3.数组forEach map filter find
var names = ["ABC",'小余','coderwhy']
names.forEach(function(){
console.log("item",this);
})
//返回连续3个window
//如果forEach加上了第二个参数,则this指向就会发生改变,因为绑定的对象已经被我们手动设置了,同理的map filter find 这些数组的高阶函数都差不多
names.forEach(function(){
console.log("item",this);
},"小余")
-
来分别探讨一下两种情况,如图8-13与8-14的控制台结果
图8-13 forEach不加第二个参数
图8-14 forEach加第二个参数 -
而这一系列的方法之中,基本上最后一个参数都是用来绑定this的,并且我们不需要去记忆,在编辑器敲击出来的是,记得看提示就OK了
-
这个提示的来源是TS,后期如果学习了TS,就能对这个语法有更清晰的了解了,如图8-15
-
图8-15 编辑器提供的语法提示
-
接下来,我们来测试下大多数的方式,进行对比
-
根据图8-16所对比的结果和写法是没有差别的,这能有效降低我们理解的心智负担
-
names.forEach(function(){
console.log("forEach",this);
},"小余")
names.map(function(){
console.log("map",this);
},"小余")
names.filter(function(){
console.log("filter",this);
},"小余")
names.find(function(){
console.log("find",this);
},"小余")
图8-16 forEach map filter find高阶函数对比情况
四、this规则优先级
学习了四条规则,接下来开发中我们只需要去查找函数的调用应用了哪条规则即可,但是如果一个函数调用位置应用了很多条规则,优先级谁更高呢?
-
默认规则的优先度是最低的
-
毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定this
-
-
显式绑定优先级高于隐式绑定
//代码测试
var obj = {
name: "小余",
foo: function () {
console.log(this);
}
}
obj.foo()//单纯隐式绑定
//{name: '小余', foo: ƒ}
//1.call、apply的显式绑定高于隐式绑定
obj.foo.call("我是coderwhy")//显式绑定跟隐式绑定的冲突
//String {'我是coderwhy'}
//2.bind与隐式绑定的优先度比较
var bar = obj.foo.bind("小余666")
bar()//String {'小余666'}
-
bind更明显的比较案例
-
我们在obj1内部对foo方法进行显式绑定
-
然后在以obj1进行隐式调用
-
看显式和隐式之间的规则冲突下,谁的优先度更高
-
-
根据最终的结果,是显式绑定"coderwhy"。说明了
显式绑定
优先度更高
//更明显的比较
function foo(){
console.log(this)
}
var obj1 = {
name:"这是bind更明显的比较",
foo:foo.bind("coderwhy")
}
obj1.foo()//此时的foo属性才是被绑定到bind上面,前面刚开始的优先度比较更像是直接调用bind传入bar的,不够公平
//[String: 'coderwhy']
//答案返回的是coderwhy,所以bind的显式绑定优先度也比obj1的隐式绑定更高
-
new绑定优先级高于隐式绑定
-
如果this打印出来的是obj对象,则证明隐式绑定的优先度更高,如果是foo创建出来的函数对象,则证明new的优先度更高
-
-
通过图8-17的结果,确定了new绑定优先度更高
//代码测试
var obj = {
name:"coderwhy的JS高级课程很不错,强烈推荐来看",
foo:function(){
console.log(this);
}
}
var f = new obj.foo()//答案为:foo {}
//foo {} 是foo创建出来的函数对象,证明了new的优先度更高
obj.foo()//这是隐式绑定的写法,可以方便进行对比
图8-17 new绑定优先级高于隐式绑定
-
结论:new关键字是不能够跟call和apply一起来使用的
-
因为call、apply跟new一样都是主动的去调用函数的,是不能够放在一起来使用。所以我们只能够将bind跟new来进行比较,这证明了一件事,那就是bind不是主动去调用函数的(他虽然也改变了this指向,但是会返回新的内容且需要我们去调用,并且不影响之前的内容),下方的案例也说明了这点
-
new关键字内部在去执行的时候,会找到原函数的,将原来的函数当作一个构造器(构造器的概念讲到后面的面向对象的部分会详细讲解),这就是为什么new出来的bar函数最终还是调用foo函数的原因
-
function foo(){
console.log(this);
}
var bar = foo.bind("测试一下")
//new出来的bar函数最终还是调用foo函数的
var obj = new bar()//foo{}
bar()//[String: '测试一下']
4.1. 优先度总结
表8-1 this优先度总结表
绑定类型 | 描述 | 优先级 |
---|---|---|
New 绑定 | 当函数或方法被作为构造函数使用时,使用new 关键字调用,this 绑定到新创建的对象上 | 最高 |
显式绑定 | 使用.apply() 、.call() 或.bind() 方法直接指定this 的上下文。bind() 方法返回的是新函数 | 中高 |
隐式绑定 | 当函数作为对象的方法调用时,this 绑定到该对象。如果函数在多层对象内,this 指向最近一层的对象 | 中低 |
默认绑定 | 在非严格模式下,单独调用函数时,this 指向全局对象(浏览器中的window ,Node.js中的global );在严格模式下,this 为undefined | 最低 |
五、this规则之外
我们讲到的规则已经足够应付平时的开发了,但是总有一些语法,超出我们的规则之外。
5.1. 特殊绑定-忽略显式绑定
-
apply、call、bind:当传入
null/undefined
时,自动绑定成全局对象-
显式绑定的三种方法在
null/undefined
的情况下,就会被忽略,变为最基础的默认绑定
-
function foo(){
console.log(this);
}
foo()
foo.apply(null)
foo.apply(undefined)
//打印出来全部都是window,我们可以看到填入 null跟undefined打印出来的也是全局的对象
5.2. 特殊绑定-间接函数引用
-
另外一种情况:
创建一个函数的间接引用,这种情况使用默认绑定规则
-
赋值表达式(obj2.foo = obj1.foo)的结果返回值是目标函数的引用,因此调用的位置是
foo()
,而不是obj2.foo()
或者obj1.foo()
-
foo函数被直接调用,那么就是默认绑定
-
间接引用最容易发生在赋值的时候
-
该特殊绑定情况是一种独立函数调用,将obj2.foo = obj1.foo作为一个整体来调用。这种情况叫做间接引用,我们并没有直接拿到这个函数,而是通过obj2.foo = obj1.foo这个表达式来返回函数,然后对这个函数做一个调用。这种情况也属于独立函数的调用
//争论:代码规范,到底加不加分号;
var obj1 = {
name:"这是onj1",
foo:function(){
console.log(this);
}
}
var obj2 = {
name:"这是obj2",
}
obj2.foo = obj1.foo
obj2.foo()//{ name: '这是obj2', foo: [Function: foo] }
//第二种情况,比较难的情况
(obj2.foo = obj1.foo)()
第二种情况特殊情况(了解就行,一般没人这么写)
-
如果我们不再obj2对象结束那里加上分号的话,编辑器会连带这下面的调用当作一个整体,这是语法分析的一个问题
-
像
(obj2.foo = obj1.foo)()
这样写,其实就是把obj2.foo = obj1.foo
当做一个整体进行执行-
()
的两种作用进行了结合,第一种作用是视为整体,第二种作用是执行 -
结合起来就会产生这种效果,但一般来说,我们确实不会这么写,这种代码会给人带来较大的理解负担,并不实用
-
var obj2 = {
name:"这是obj2",
}
//会将obj2对象连着下面调用当作一个整体
//第二种情况,比较难的情况
(obj2.foo = obj1.foo)()
----------------------------------
//相当于变成如下情况
var obj2 = {
name:"这是obj2",
}(obj2.foo = obj1.foo)()
//会报错:Uncaught TypeError: Cannot set properties of undefined (setting 'foo')
---------------------------------------
//加上分号后:
var obj2 = {
name:"这是obj2",
};
(obj2.foo = obj1.foo)()
//正常返回window
-
最后提一嘴,箭头函数也不受this规则的影响,但由于我们马上就要在下一篇章中进行详细讲解了,这里就进行略过
5.2.1. 测试代码(来自《你不知道的JavaScript》)
-
这段测试代码主要测试了我们对语法和this的熟悉程度
function foo(el){
console.log(el,this);
}
var obj = {
id:"XiaoYu"
}
[1,2,3].forEach(foo,obj)
//无法运行
//报错:Uncaught TypeError: Cannot read properties of undefined (reading 'forEach')
---------------------------------------
//解决方法1:
function foo(el){
console.log(el,this);
}
var obj = {
id:"XiaoYu"
}
var names = [1,2,3]
names.forEach(foo,obj)
------------------------------------------
//解决方法2:
function foo(el){
console.log(el,this);
}
var obj = {
id:"XiaoYu"
};//加上分号,不然会将obj对象和底下的当作一个整体
[1,2,3].forEach(foo,obj)//foo是我们在上面独立定义了,obj是我们传入forEach中this要绑定的对象
-
原始代码的问题在于 JavaScript 解析器将
[1,2,3].forEach(foo,obj)
解释为了一个属性访问表达式[1,2,3].forEach(foo
和一个标签语句obj)
,导致语法错误。foo和obj中间的逗号被错误解析解决方法1:
解决方法2:
-
在
obj
对象声明后显式添加一个分号;
,确保obj
的声明被正确地结束。避免了 JavaScript 的自动分号插入(ASI)机制可能导致的问题,其中解释器可能不会在obj
声明之后自动插入分号 -
通过确保
obj
声明后有分号,可以安全地在数组[1, 2, 3]
上调用forEach
方法,并将this
上下文绑定到obj
,而不会引起语法解析错误 -
将数组存储在变量
names
中后,是明确地结束了这一声明语句,JS代码会在回车换行后自动在变量后面加上;
。这样做可以避免解析器将当前的数组字面量解释为前一语句的一部分 -
JavaScript中实现在代码结尾自动加分号的机制叫做自动分号插入(Automatic Semicolon Insertion, ASI)。这是JavaScript解析器的一个特性,允许省略某些情况下的分号,解析器会根据特定的规则自动补上
-
我们要避免直接字面量操作的解析问题,在JS中,如果连续的语句没有通过分号明确分隔,数组直接跟在一个对象字面量后面可能被解释为尝试访问对象的属性或者进行函数调用
-
而这也是社区上对于要不要在语句中加
;
的争论点,大多数情况下,只要我们注意规范使用,是不会出现这些问题的,但另一部分人的看法则是要自己来进行,会更严谨一些。因为在复杂的表达式和返回语句中,也可能导致一些难以预料的行为 -
这是一个仁者见仁,智者见智的问题
-