JS中函数的this指向(一)https://developer.aliyun.com/article/1470343
规则4:new绑定
- JavaScript中的函数可以当作一个类的构造函数来使用,也就是使用new关键字
- 使用new关键字来调用函数是,会执行如下的操作:
- 创建一个全新的对象
- 这个新对象会被执行prototype连接
- 这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成)
- 如果函数没有返回其他对象,表达式会返回这个新对象
new来调用,会把我们生成的新的对象赋值给这个Person里面内部的的this
准确来说,js 中的构造函数只是使用new 调用的普通函数,它并不是一个类,最终返回的对象也不是一个实例,只是为了便于理解习惯这么说罢了。
那么new一个函数究竟发生了什么呢,大致分为三步:
- 以构造器的prototype属性为原型,创建新对象;
- 将this(可以理解为上句创建的新对象)和调用参数传给构造器,执行;
- 如果构造器没有手动返回对象,则返回第一步创建的对象
function Person(){ console.log(this); } Person()//正常调用 new Person()//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("小满",23) console.log(p2.name,p2.age); //小余 20 //小满 23
一些函数的this分析
内置函数的绑定思考
- 有些时候,我们会调用一些JavaScript的内置函数,或者一些第三方库的内置函数
- 这些内置函数会要求我们传入另外一个函数;
- 我们自己并不会显示的调用这些函数,而且JavaScript内部或者第三方库内部会帮助我们执行;
- 这些函数中的this又是如何绑定的呢?
- setTimeout、数组的forEach、div的点击
setTimeout定时器
setTimeout(function(){ console.log("正常的this",this);//window }) setTimeout(()=>{ console.log("箭头函数的this",this);// },2000)
node环境下的结果:
监听点击
css样式不写,自己写一个宽高加背景颜色出来方便点击
我们监听点击中的this给到我们的是监听的对象,也就是如下的东西(div的元素对象):
this指向了div的元素对象,这说明了这个boxDiv会拿到内部的函数的,然后进行调用,相当于
也就是隐式绑定了(只不过内部进行了,没有显示出来),将boxDiv绑定到了onclick上面,所以this会绑定到div元素对象上
boxDiv.onclick()
<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); })
数组中的绑定
正常情况下是返回window,但是forEach是接收第二个参数,第二个参数可以帮我们绑定对象,也就包括了this的指向位置
//3.数组forEach map filter find var names = ["ABC",'小余','小满'] names.forEach(function(){ console.log("item",this); }) //返回连续3个window //如果forEach加上了第二个参数,则this指向就会发生改变,因为绑定的对象已经被我们手动设置了,同理的map filter find 这些数组的高阶函数都差不多 names.forEach(function(){ console.log("item",this); },"小余")
forEach不加第二个参数:
forEach加第二个参数:
其他函数的效果(不一定就这些):
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); },"小余")
this规则优先级
- 学习了四条规则,接下来开发中我们只需要去查找函数的调用应用了哪条规则即可,但是如果一个函数调用位置应用了很多条规则,优先级谁更高呢?
- 默认规则的优先度是最低的
- 毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定ths
- 显示绑定优先级高于隐式绑定
- 代码测试
var obj = { name:"小余", foo:function(){ console.log(this); } } obj.foo()//单纯隐式绑定 //{ name: '小余', foo: [Function: foo] } //1.call、apply的显示绑定高于隐式绑定 obj.foo.call("我是小满")//显示绑定跟隐式绑定的冲突 //[String: '我是小满'] //2.bind与隐式绑定的优先度比较 var bar = obj.foo.bind("小余666") bar()
bind更明显的比较
//更明显的比较 function foo(){ console.log(this) } var obj1 = { name:"这是bind更明显的比较", foo:foo.bind("喜多川") } obj1.foo()//此时的foo属性才是被绑定到bind上面,前面刚开始的优先度比较更像是直接调用bind传入bar的,不够公平 //[String: '喜多川'] //答案返回的是喜多川,所以bind的显示绑定优先度也更高
- new绑定优先级高于隐式绑定
- 代码测试
如果this打印出来的是obj对象,则证明隐式绑定的优先度更高,如果是foo创建出来的函数对象,则证明new的优先度更高
var obj = { name:"小满Vue3视频讲得不错", foo:function(){ console.log(this); } } var f = new obj.foo() //foo {} 是foo创建出来的函数对象,证明了new的优先度更高 obj.foo()//这是隐式绑定的写法,可以方便进行对比
隐式绑定的结果应该是下面这样的:
- 结论: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: '测试一下']
优先度总结
new绑定>显示绑定(apply、call、bind)>隐式绑定>默认绑定(独立函数调用 )
bind高于call(一般情况下也不会同时用这两个,当作一个了解即可)
this规则之外
- 我们讲到的规则已经足够应付平时的开发了,但是总有一些语法,超出我们的规则之外。
特殊绑定--忽略显示绑定
apply、call、bind:当传入null/undefined时,自动绑定成全局对象
function foo(){ console.log(this); } foo() foo.apply(null) foo.apply(undefined) //打印出来全部都是window,我们可以看到填入 null跟undefined打印出来的也是全局的对象
特殊绑定--间接函数引用
- 另外一种情况,创建一个函数的间接引用,这种情况使用默认绑定规则
- 赋值(obj2.foo = obj1.foo)的结果时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对象结束那里加上分号的话,编辑器会连带这下面的调用当作一个整体,这是语法分析的一个问题
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
测试代码(来自你不知道的JavaScript)
function foo(el){ console.log(el,this); } var obj = { id:"I am is 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:"I am is XiaoYu" } var names = [1,2,3] names.forEach(foo,obj) ------------------------------------------ //解决方法2: function foo(el){ console.log(el,this); } var obj = { id:"I am is XiaoYu" };//加上分号,不然会将obj对象和底下的当作一个整体 [1,2,3].forEach(foo,obj)//foo是我们在上面独立定义了,obj是我们传入forEach中this要绑定的对象
箭头函数arrow function
- 箭头函数是ES6之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁:
- 箭头函数不会绑定this、argument属性
- 箭头函数不能作为构造函数来使用(不能和new一起来使用,会抛出错误)
箭头函数的使用解析
编写箭头函数
():参数
=>:箭头
{}:函数执行体(在一些特殊场景大括号可以省略 )
//方式1: var nums = [10,20,30,40] nums.forEach((num1,num2,num3)=>{ console.log(num1,num2,num3) }) ---------------------------------------------------------- //方式2(完整写法): var foo = (num1,num2,num3)=>{ console.log(num1,num2,num3) } var nums = [10,20,30,40] nums.forEach(foo)
箭头函数常见简写方法
//简写1:如果参数只有一个,小括号可以省略 //简写前: nums.forEach((item)=>{ console.log(item) }) //简写后: nums.forEach(item=>{ console.log(item) }) //简写2:如果执行体只有一个,大括号可以省略 nums.forEach(item => console.log(item)) //强调:并且它会默认将这行代码的执行结果作为返回值 var newNums = nums.filter(item => item % 2 === 0)//item % 2 === 0的结果会默认返回 console.log(newNums) //一般情况下我们带大括号的是需要手动return返回的,就像这样 var newNums = nums.filter(item => { return item % 2 === 0//这种有大括号的情况下,如果我们不return的话,会返回[] })
filter/map/reduce结合使用
用一行完成了对初始值的:过滤出偶数并将其每个偶数扩大100倍,使其相加
var nums = [10,20,30,40,51] // filter/map/reduce结合使用 var result = nums.filter(item => item % 2 === 0) .map(item => item *100) .reduce((preValue,item)=>preValue+item) console.log(result)
//简写3:如果一个箭头函数,只有一行代码,并且返回一个对象,这个时候如何编写简写 var bar = ()=>{ return { name:"小余", age:20 } } //如果你按照上面的简写思路的话,那应该是 var bar = ()=> {name:"小余",age:18}//但这种写法其实是错误的,因为这里会发生混乱,这个大括号到底是判定为执行体还是对象呢?JS引擎会发生错乱 //正确的简写方式 var bar = ()=> ({name:"小余",age:18}) //使用小括号将对象包裹起来,这个是将对象当作一整个整体
箭头函数的this获取
箭头函数不会绑定this
- 为什么3种不同方式的调用都是window,首先那是因为我们foo的上层作用域是全局的,全局的可不就是window,然后就是我们的call怎么没有改变成功this的指向呢?那是因为箭头函数的原因,箭头函数不会绑定this属性,而我们的foo函数恰巧使用了箭头函数,造成了所有的绑定效果都是指向window,这是很有用的一个特点
var name = "小余" var foo = ()=>{ console.log(this); } foo()//window var obj = {foo:foo} obj.foo()//window foo.call("这是call调用的")//window
有无使用箭头函数的this对比
//对比前,没有使用箭头函数 var name = "小余" function foo(){ console.log(this); } // foo() var obj = {name:"你已经被小余绑定到obj上啦",foo:foo} obj.foo()//{ name: '你已经被小余绑定到obj上啦', foo: [Function: foo] } ----------------------------------------------------------------------------------------------- //对比后,使用箭头函数 var name = "小余" var foo = ()=>{ console.log(this); } var obj = {name:"你已经被小余绑定到obj上啦",foo:foo} obj.foo()//window
箭头函数应用场景
发送网络请求,将结果放到上面data属性中
在没有箭头函数的时候,我们通过在getData中创建一个变量将getData内的this的指向进行一个接收来进行使用
有了箭头函数就不需要这么麻烦,因为箭头函数没有绑定this,所以我们在调用obj.getData的时候,this不会被隐式绑定给强行改变,没有被改变的话,当this在当层找不到想要的就会直接去自己的上层找
//无箭头函数时候 var obj = { data:[], getData:function(){ //在没有箭头函数的时候,大家通常是这么解决问题的 var _this = this//这里的this就是obj对象了,getData的上一层可不就是obj setTimeout(function(){//没有使用箭头函数,会出现问题,所以要加上var _this = this,然后使用_this var result = ["小余",'小满','康老师'] _this.data = result//_this是外层的变量,这里就形成了一个闭包 console.log(this) },2000) } } obj.getData()//没有使用箭头函数或者没有声明一个变量来接收getData里面的this的时候,为什么是window,那是因为foo函数绑定到obj上面啦,obj的上层就是全局window了,这是隐式绑定
//有箭头函数的时候 var obj = { data:[], getData:function(){ setTimeout(()=>{ var result = ["小余",'小满','康老师'] this.data = result//直接使用this console.log(this)//通过直接打印this进行检测 },2000) } } obj.getData()
this面试题
面试题1
//无答案解析版本 var name = "window" var person = { name:"person", sayName:function(){ console.log(this.name);//这里的答案是谁 } }; function sayName(){ var sss = person.sayName sss();//调用打印出来的是什么 person.sayName();//? (person.sayName)();//? (b = person.sayName)();//? } sayName()
//答案解析版本 var name = "小余window" var person = { name:"person", sayName:function(){ console.log(this.name); } }; function sayName(){ var sss = person.sayName sss();//this.name是小余window ,独立函数调用,所以这里的this指向最外层的window person.sayName();//隐式调用,this指向person,控制台打印的this.name是person (person.sayName)();//person,隐式调用 (b = person.sayName)();//间接函数引用,是独立的函数调用,所以是小余window,(b = person.sayName)是一个整体 } sayName()
面试题2
var name = 'window' //person1是字面量对象 var person1 = {//定义对象的时候是不会产生作用域的,所以对象里面的上层在对象外面 name: 'person1', foo1: function () { console.log(this.name) },//普通函数 foo2: () => console.log(this.name),//箭头函数 foo3: function () { return function () { console.log(this.name) } },//函数套函数,返回普通函数 foo4: function () { return () => { console.log(this.name) } }//函数套函数,返回箭头函数 } var person2 = { name: 'person2' } // person1.foo1(); // person1(隐式绑定) // person1.foo1.call(person2); // person2(显示绑定优先级大于隐式绑定) // person1.foo2(); // window(不绑定作用域,上层作用域是全局) // person1.foo2.call(person2); // window //这里的person1.foo3()的调用下拿到结果在()继续调用,这种属于独立调用 // person1.foo3()(); // window(独立函数调用) // person1.foo3.call(person2)(); // window(独立函数调用) // person1.foo3().call(person2); // person2(最终调用返回函数式, 使用的是显示绑定) // person1.foo4()(); // person1(箭头函数不绑定this, 上层作用域this是person1) // person1.foo4.call(person2)(); // person2(上层作用域被显示的绑定了一个person2) // person1.foo4().call(person2); // person1(上层找到person1)
面试题3
- 连续new了两次,代表构造函数会被连续调用两次
- 每次new的时候都会创建一个新的对象,这里new了两次表示创建了两个新的对象
var person1 = new Person('person1') var person2 = new Person('person2') //创建出来的2个新对象: this = {name:"person1",foo1:function{}} this = {name:"person2",foo1:function{}}
var name = 'window' function Person (name) {//作为构造函数,一般情况下,我们都开头字母大写 this.name = name this.foo1 = function () { console.log(this.name) }, this.foo2 = () => console.log(this.name), this.foo3 = function () { return function () { console.log(this.name) } }, this.foo4 = function () { return () => { console.log(this.name) } } } var person1 = new Person('person1') var person2 = new Person('person2') person1.foo1() // person1 person1.foo1.call(person2) // person2(显示高于隐式绑定) person1.foo2() // person1 (上层作用域中的this是person1) person1.foo2.call(person2) // person1 (上层作用域中的this是person1) person1.foo3()() // window(独立函数调用) person1.foo3.call(person2)() // window person1.foo3().call(person2) // person2 person1.foo4()() // person1 person1.foo4.call(person2)() // person2 person1.foo4().call(person2) // person1 var obj = { name: "obj", foo: function() { } }
面试题4
通常我们会对什么时候调用感到疑惑,例如下面这两个
- 他们的区别从foo2开始发生不同,foo2.xxx表示到了foo2还没调用,而是继续深入到里面
- foo2()则是调用了,然后foo2属性对应的将会生效替代foo2()部分,变为foo2().call(person2)
- 在下面的表达式中,foo2()属性调用则是return了一个箭头函数,既然return了,那就跳出外面一层function了,且箭头函数是不受call改变this的,this的指向当然就是obj咯(return出来的函数的上一层或者说父级作用域就是obj函数),所以this.name自然就是obj对象里面的name:obj了
person1.obj.foo2.call(person2)() // person2 person1.obj.foo2().call(person2) // obj
var name = 'window' function Person (name) { this.name = name this.obj = {//对象里面封装对象 name: 'obj', foo1: function () { return function () {//普通返回 console.log(this.name) } }, foo2: function () { return () => {//箭头函数返回 console.log(this.name) } } } } var person1 = new Person('person1') var person2 = new Person('person2') person1.obj.foo1()() // window person1.obj.foo1.call(person2)() // window person1.obj.foo1().call(person2) // person2 person1.obj.foo2()() // obj person1.obj.foo2.call(person2)() // person2 person1.obj.foo2().call(person2) // obj // // 上层作用域的理解 // var obj = { // name: "obj", // foo: function() { // // 上层作用域是全局 // } // } // function Student() { // this.foo = function() { // } // }