这是一篇关于 this 指向问题的分享会文档
必要知识准备
介绍 this 之前,先简单介绍四个概念(普通函数、对象方法、构造函数、箭头函数),作为我们的知识储备,为后面我们介绍 this 做铺垫
普通函数:函数是一段可以被重复调用的代码块,普通函数的声明方法基本结构很简单,我们都知道,就是下面的这种
function函数名(){函数体}// 调用函数函数名()对象方法
什么是对象?
概念:对象也是 JS 数据类型的一种,和之前学习的数值类型、字符串类型、布尔类型是一样的。对象数据类型可以被理解成是一组无序的 键值对的集合,是属性的容器。
- 键: 又称属性名,通常是一个字符串
- 值: 可以是任何数据类型,包括:数字、字符串、布尔值、数组、函数,甚至是另一个对象(嵌套)
- 属性:属性是成对出现的,包括属性名和属性值,它们之间使用英文
:分隔,多个属性之间使用英文,分隔
怎样创建一个对象呢?
- 最常用的一种就是使用花括号
{ }直接创建
let对象名={属性名1:属性值1,属性名2:属性值2,属性名3:属性值3,属性名4:函数,...}什么是方法?
- 属性值可以是任何数据类型,这里面有一个数据类型比较特殊,就是函数,如果一个属性的值是一个函数,那么我们称这个属性是这个对象的一个方法
由于普通函数和方法之间有点像,所以我们举个例子带大家区分一下
// 创建一个函数functionfn(){console.log('111')}// 直接放在script标签里面,他就是一个普通函数functionfn(){console.log('111')}// 将这个函数放进一个对象里面letobj={fn:function(){// 是这个obj对象的方法console.log('111')}}怎样访问对象里面的属性呢?
创建了一个对象之后,我们怎么访问该怎样访问这个对象里面的属性呢?
声明对象并添加了若干属性后,可以使用
.或[]获得对象中属性对应的值,基本上写法就是对象名.属性名或者对象名[‘属性名’]
如果我们需要为这个属性重新赋值或者添加一个属性,也很简单对象名.属性名 = 新的属性值
我们平常写代码经常写的
// 获取DOM元素letoDiv=document.querySelector('div')// 给这个元素添加一个字体颜色为红色的样式oDiv.style.color='red'// style 就是 oDiv 的一个属性,这个属性的值其实也是一个对象,而 color 是 style 的一个属性上面的是不是很熟悉,其实当我们把
div这个标签获取过来并赋给变量oDiv时,我们也相当于创建一个一个对象名为oDiv的对象,只是这个对象里面已经很多自带的属性和方法了,我们用的时候直接使用.调用我们需要的属性,并给它重新赋值就行了怎样访问对象里面的方法呢?
- 基本上和访问属性的写法是一致的,只有一点不一样,我们都知道普通函数的调用方法,
函数名(),最后必须要加上一个小括号,这个函数的调用的方法是不论在哪里都不变的,前面讲到方法的属性值是函数,既然是函数,那我们也应该在属性名后面加上(),即对象名.属性名()
letperson={name:'小红',age:18,singing:function(){console.log('两只老虎,两只老虎,跑的快,跑的快...')},run:function(){console.log('我跑的非常快...')}}// 调用person对象中 singing 方法person.singing()// 调用person对象中的 run 方法person.run()构造函数
在前面我们学习了对象之后,应该能更好的理解构造函数,因为构造函数其实也是在创建对象
什么是构造函数?
本质上就是一个普通的函数,但是会有两个地方不太一样
命名方面上,构造函数的名称首字母要大写,这其实是一种普遍的命名的约定用来区别普通函数
与
new操作符一起使用:构造函数通过new操作符来调用,用于创建并初始化一个新对象。你们也可以这样理解,当一个函数使用new操作符调用时,它就成为了“构造函数”,即一个用于构造新对象的函数
构造函数的作用:创建对象
在没有构造函数之前,我们使用对象字面量创建单个对象
// 对象字面量创建单个对象letperson1={name:"Alice",age:25,greet :function(){console.log(Hello,my name is11);}}person1.greet();// 输出: Hello, my name is 11但如果要创建多个结构相似(都有 name, age, greet 属性)的对象,重复写对象字面量会很麻烦,这时构造函数就派上用场了
构造函数就像一个“工厂模具”,可以批量生产具有相同属性和方法结构的对象
怎样使用构造函数呢?
- 第一步:定义构造函数,在其内部使用
this来定义未来的实例对象里面的属性和方法
创建一个函数(首字母要大写)
// 1. 定义构造函数 (首字母大写)// 这里相当于创建了一个模板functionPerson(name,age){// 2. 使用 this 来添加属性this.name=name;this.age=age;}- 第二步:使用
new调用构造函数,使用new操作符来调用Person函数,它会按照构造函数的模板创建一个新的对象并返回它
- 第一步:定义构造函数,在其内部使用
// 使用 new 创建实例// 我们在这里调用这个模板letperson1=newPerson('Alice',25);letperson2=newPerson('Bob',30);console.log(person1.name);// 输出: Aliceconsole.log(person2.age);// 输出: 30我们可以看一个例子:
到这里,我想问大家,现在大家知道上面第二个图片里的变量
person1和person2的值是什么数据类型吗?箭头函数
箭头函数是 ES6 中引入的一种新的函数语法,它提供了一种更简洁的方式来编写函数
基本语法:
- 我们与传统的函数表达式对比来学
constadd=function(a,b){returna+b}// 完整写法(多行语句或需要显式返回对象时)constadd=(a,b)=>{returna+b}// 简洁写法(单行表达式,隐含 return)constadd=(a,b)=>a+b大家观察上面的两种写法有什么不同?
- 仔细观察,赋值等号左边的没有变化,变化在右边,箭头函数删去了 function ,变成了 => ,并且将 => 放到了 小括号 和 大括号 的中间
好啦,到这里我们的前置只知识准备就完成了,这就开始我们第二部分的介绍吧!
this 到底什么?
在正式开始介绍 this 到底是什么之前,这里有两个问题:
this 是不是变量?
- 不是,
this是一个特殊关键字(像function,if,new一样),不是可以声明的变量或对象的属性
- 不是,
this 是对象吗?学到现在,大家应该或多或少都应该见过
this.sayName()这种写法,那按照我们上面对于对象方法的学习,大家觉得this 是对象吗?- 也不是,this 很多变,它本身不是一个对象,只是它的值实际根据是谁在调用它而变的,只是一般情况下 this 的值(它所指向的东西),总是一个对象
有一个类比,万能电视遥控器(this)
- 万能遥控器(this)本身不是电视,但按下遥控器的按钮(调用 this)的效果,取决于它当前指向并控制的是哪台电视,你可以用同一个遥控器(this),通过改变它的指向(不同的调用方式)来控制不同的电视(不同的对象)
所以 this 到底是什么呢?
一般来说,this 都是放在一个函数中使用的,调用这个函数也就相当于在调用这个 this ,
当我们总结上面的问题和类比例子后,其实可以用一句话总结,就是
this的值不是在我们定义函数时确定的,而是在我们调用这个函数时才被确定的一个贴近生活的比喻:
想象你是一个员工(函数),你有一句话(函数体):“我正在为
this公司工作“如果你在A公司的办公室里说这句话,
this就代表 A公司如果你在B公司的办公室里说这句话,
this就代表 B公司你(函数)本身没变,但你所在的环境(调用者)变了,this的含义也就随之改变了
- 说多不如练多,这里有一个小问题,大家可以先试试能不能解答
constperson={name:'小明',sayHi:function(){console.log('你好,我是'+this.name)}}person.sayHi()六个常见环境下的 this 指向问题
全局环境中的 this
- 全局环境就是在
<script></script>里(不在任何函数或对象内部),此时的 this 始终指向的是全局对象,在浏览器中就是 window
console.log(this)- 全局环境就是在
普通函数中的 this
当一个函数被直接调用时,要考虑两种情况
this 在非严格模式下指向全局对象(window)
functionfun(){console.log(this)}fun()// 我们实际上是在全局作用域下直接调用函数// fun() 实际上是window.fun(), 所以this -> window在严格模式下指向undefined
JS 严格模式:JavaScript 在语法和行为上存在一些模糊的特性,可能导致一些不易察觉的错误,为提高代码的质量和可维护性,JS 引入了严格模式,通过启用一些额外的规则,强制执行更严格的语法和行为,帮助提前发现和修复潜在 bug。
// 演示严格模式下的情况functionfu(){"use strict"// 在函数体中写一句 "use strict" ,就可以启用函数的严格模式console.log(this)}fu()// this 指向 undefined对像方法中的this
- 当函数作为对象的方法被调用时,this指向该方法所属的对象
letperson={name:"John",sayName:function(){console.log(this.name)}}person.sayName()letname='卡卡';letcat={name:'有鱼',eat1:{name:'年年',eat2:function(){console.log(this.name);}}}cat.eat1.eat2();构造函数中的this
- 使用 new 关键字(实例化)调用函数时,该函数被当作构造函数,this 会指向新创建的对象实例
// 构造函数functionPerson(name){this.name=namethis.sayHello=function(){console.log("Hello, I'm "+this.name)}}// 创建实例letjohn=newPerson("John");john.sayHello()// 输出 "Hello, I'm John",这里 this 指向 john 实例letjohn={name:name,sayHello:function(){console.log("Hello, I'm "+this.name)}}// 构造函数functionPerson(name){this.name=namethis.sayHello=function(){console.log(`你好,我是${this.name}`)}}// 创建实例constperson1=newPerson('张三')person1.sayHello()事件处理中的 this
- 在 DOM 事件处理函数中,this 通常指向触发事件的元素
<button id="myButton">Click me</button><script>varbutton=document.getElementById("myButton")button.onclick=function(){console.log(this)}</script><buttonclass="btu">Click</button><script>letoBtu=document.querySelector('.btu')oBtn.addEventListenter('click',clickBtu)functionclickBtu(){console.log(this)}</script>箭头函数中的 this
箭头函数没有自己的this,它的 this 直接“捕获”或“继承”**外层作用域(它父级)**的 this 的值
前面介绍过普通函数 的
this是动态的,取决于如何被调用而箭头函数 的
this是词法的,取决于定义时的上下文,且一旦定义就固定不变词法的 = 写代码时候的位置决定的(this的出生地)
// 普通函数functionouterFunction(){this.name="Outer"varinnerFunction=function(){console.log(this.name)}innerFunction()}// 箭头函数functionouterFunctionWithArrow(){this.name="外层作用域的this"varinnerFunction=()=>{console.log(this.name)}innerFunction()}// 一种情况newouterFunction()newouterFunctionWithArrow()// 另一种调用情况outerFunction()outerFunctionWithArrow()改变 this 指向的方法
由于箭头函数的this来自于继承,箭头函数无法使用以下三种方法改变this指向
call()方法call()方法附加在函数调用后面使用,可以忽略函数本身的 this 指向,然后将这个函数本身的 this 指向绑定到call()方法的第一个参数上面函数.call(thisArg,arg1,arg2,...,argN)参数解析:
thisArg:就是你想将调用这个方法的函数本身的 this 指向绑定到哪个对象上面arg1-argN:从参数arg1开始,依次是向函数传递参数
注意点:
- 使用
call()方法时,相当于你调用了这个函数,会立刻执行这个函数
- 使用
functiongreet(){console.log(this.animal,"的睡眠时间一般在",this.sleepDuration,"之间");}constobj={animal:"猫",sleepDuration:"12 到 16 小时",};greet.call(obj);// 猫 的睡眠时间一般在 12 到 16 小时 之间apply()方法和
call()方法很相似,几乎一样函数名.apply(thisArg,[arg1,arg2,...,argN])参数解析:
thisArg:同样为你想将这个函数本身的 this 指向绑定到哪个对象上面[arg1,arg2,...,argN](主要区别点):一个数组,数组里面的每一项依次是向函数传递的参数,这里可以直接写入一个数组,也可以写一个值为数组的变量
bind()方法和上面两个方法不一样,
bind()方法创建一个新函数并且还有返回值,使用bind()方法后不会立即执行函数,而是返回一个已经改变了 this 指向的函数函数名.bind(thisArg,arg1,arg2,..,argN)参数解析:
thisArg:作用与前两个一致arg1, arg2, ..,argN: 在调用函数时,插入到传入绑定函数的参数前的参数
返回值:
- 一个改变了 this 指向以后的 function 函数
constmodule={x:42,getX:function(){returnthis.x;},};console.log(module.getX())constunboundGetX=module.getX;//undefined 函数未被调用,存储的是一个值console.log(unboundGetX());constboundGetX=unboundGetX.bind(module);console.log(boundGetX());总结及综合演练
this 指向:
1、一般函数:谁调用函数,this 就指向谁,没有调用者就指向全局对象 Window
2、箭头函数:箭头函数不会创建 this,它的 this 继承自上层作用域中的 this
<buttonclass='btu'>Click</button><script>console.log(this)// 一个普通函数functionfn(){console.log(this)}// 创建一个对象letobj={fn:fn}// 直接调用fn()// 对象方法调用obj.fn()// 事件处理中调用letoBtu=document.querySelector('.btu')oBtu.addEventLister('click',fn)</script>// 构造函数functionFn(age){this.name=111this.age=age,this.currentAge=function(){console.log(this.age)}}letFn1=newFn(16)Fn1.currentAge()// call()方法functionProduct(name,price){this.name=name;this.price=price;}functionFood(name,price){Product.call(this,name,price);this.category="food";}console.log(newFood("cheese",5).name);代码解析:
关键点:
Product.call(this, name, price)这行代码的作用是:
在 Food 的上下文中调用 Product 构造函数
第一个参数 this 是新创建的 Food 实例
call()方法将 Product 中的 this 绑定到传入的 Food 实例上相当于在 Food 实例上设置 name 和 price 属性
执行过程:
new Food("cheese", 5)创建 Food 实例Food 构造函数中的 this 指向这个新实例
Product.call(this, "cheese", 5)让 Product 构造函数在这个 Food 实例上工作最终创建的 Food 对象包含:
{name: "cheese", price: 5, category: "food"}
constouter={name:"Outer Object",innerFunction:function(){constinner={name:"Inner Object",nestedFunction:function(){console.log(this.name)}}inner.nestedFunction()}}outer.innerFunction();