JS中函数的this指向(二)

简介: JS中函数的this指向

JS中函数的this指向(一)https://developer.aliyun.com/article/1470343

规则4:new绑定

  • JavaScript中的函数可以当作一个类的构造函数来使用,也就是使用new关键字
  • 使用new关键字来调用函数是,会执行如下的操作:
  1. 创建一个全新的对象
  2. 这个新对象会被执行prototype连接
  3. 这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成)
  4. 如果函数没有返回其他对象,表达式会返回这个新对象

new来调用,会把我们生成的新的对象赋值给这个Person里面内部的的this


准确来说,js 中的构造函数只是使用new 调用的普通函数,它并不是一个类,最终返回的对象也不是一个实例,只是为了便于理解习惯这么说罢了。


那么new一个函数究竟发生了什么呢,大致分为三步:


  1. 以构造器的prototype属性为原型,创建新对象;
  2. 将this(可以理解为上句创建的新对象)和调用参数传给构造器,执行;
  3. 如果构造器没有手动返回对象,则返回第一步创建的对象
function Person(){
    console.log(this);
}
Person()//正常调用
new Person()//new调用

调用区别:

image.png

我们通过一个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环境下的结果:

image.png

监听点击

css样式不写,自己写一个宽高加背景颜色出来方便点击


我们监听点击中的this给到我们的是监听的对象,也就是如下的东西(div的元素对象):


image.png


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不加第二个参数:

image.png

forEach加第二个参数:

image.png

其他函数的效果(不一定就这些):

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);
},"小余")

image.png

this规则优先级

  • 学习了四条规则,接下来开发中我们只需要去查找函数的调用应用了哪条规则即可,但是如果一个函数调用位置应用了很多条规则,优先级谁更高呢?
  1. 默认规则的优先度是最低的
  • 毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定ths
  1. 显示绑定优先级高于隐式绑定
  • 代码测试
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的显示绑定优先度也更高


  1. new绑定优先级高于隐式绑定
  • 代码测试

如果this打印出来的是obj对象,则证明隐式绑定的优先度更高,如果是foo创建出来的函数对象,则证明new的优先度更高


var obj = {
    name:"小满Vue3视频讲得不错",
    foo:function(){
        console.log(this);
    }
}
var f = new obj.foo()
//foo {} 是foo创建出来的函数对象,证明了new的优先度更高
obj.foo()//这是隐式绑定的写法,可以方便进行对比

隐式绑定的结果应该是下面这样的:

image.png

  1. 结论: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() {
//   }
// }


目录
相关文章
|
1月前
|
JavaScript 前端开发 Java
[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂
本文介绍了JavaScript中常用的函数和方法,包括通用函数、Global对象函数以及数组相关函数。详细列出了每个函数的参数、返回值及使用说明,并提供了示例代码。文章强调了函数的学习应结合源码和实践,适合JavaScript初学者和进阶开发者参考。
42 2
[JS]同事:这次就算了,下班回去赶紧补补内置函数,再犯肯定被主管骂
|
1月前
|
前端开发 JavaScript 开发者
除了 Generator 函数,还有哪些 JavaScript 异步编程解决方案?
【10月更文挑战第30天】开发者可以根据具体的项目情况选择合适的方式来处理异步操作,以实现高效、可读和易于维护的代码。
|
2月前
|
JavaScript 前端开发
JavaScript 函数语法
JavaScript 函数是使用 `function` 关键词定义的代码块,可在调用时执行特定任务。函数可以无参或带参,参数用于传递值并在函数内部使用。函数调用可在事件触发时进行,如用户点击按钮。JavaScript 对大小写敏感,函数名和关键词必须严格匹配。示例中展示了如何通过不同参数调用函数以生成不同的输出。
|
2月前
|
存储 JavaScript 前端开发
JS函数提升 变量提升
【10月更文挑战第6天】函数提升和变量提升是 JavaScript 语言的重要特性,但它们也可能带来一些困惑和潜在的问题。通过深入理解和掌握它们的原理和表现,开发者可以更好地编写和维护 JavaScript 代码,避免因不了解这些机制而导致的错误和不一致。同时,不断提高对执行上下文等相关概念的认识,将有助于提升对 JavaScript 语言的整体理解和运用能力。
|
3月前
|
JavaScript 前端开发 安全
JavaScript函数详解
JavaScript函数的详细解析,包括函数的定义和调用方式(如一般格式、匿名函数、构造函数、自调用函数、箭头函数和严格模式)、函数参数(arguments对象、可变参数、默认参数值)、闭包的概念和应用实例。
JavaScript函数详解
|
2月前
|
JavaScript 前端开发
js教程——函数
js教程——函数
42 4
|
2月前
|
存储 JavaScript 前端开发
js中函数、方法、对象的区别
js中函数、方法、对象的区别
21 2
|
2月前
|
JavaScript 前端开发 Java
【javaScript数组,函数】的基础知识点
【javaScript数组,函数】的基础知识点
27 5
|
2月前
|
JavaScript 前端开发
Node.js 函数
10月更文挑战第5天
24 3
|
2月前
|
前端开发 JavaScript
探索JavaScript函数基础
探索JavaScript函数基础
20 3