JS中this的绑定规则

内容总览


🤡大家好,我是一溪风月一名前端工程师,做前端两年多了,但是自己反思了一下,在这两年多的时间,在对技术的追寻上一致都是追求在这个道路上往往忽略了前端最本质的东西,那就是前端究竟在做什么,前端的价值和意义在什么地方,前端的基础,经过自己的思考决定从头学习下前端的知识,让自己沉下心来查漏补缺,放弃追逐新东西,寻找自己,了解自己。


一.直接调用(独立函数调用/全局调用)


😂直接调用顾名思义就是直接进行函数的调用,怎么直接调用哪? 我们常见的直接调用分为两种情况,如下:

js 复制代码
function foo () {
  console.log(this)
}
//进行函数调用
foo()

这是最常见的一种情况,这种情况this在非严格模式的情况下指向的是全局对象,这种调用方式,我们称之为独立函数调用,或者称之为全局调用。

还有另外一种情况是在对象中定义,然后赋值给一个变量,然后单独对这个赋值后的函数进行调用,依然是独立函数调用或者称之为全局调用,非严格模式下指向的是window。

js 复制代码
let obj = {
  name: "zpj",
  foo: function () {
    console.log(this)
  }
}
let bar = obj.foo
bar()

这种写法,依然属于独立函数调用,指向的依然是window或者说全局对象。

🚨注意:独立函数调用(全局调用)this的指向在严格模式下并不指向window而是指向undefined所以我们在使用的时候千万不要使用this来代替window,而是直接使用window。

二.对象绑定


😈通过对象绑定比较容易理解,因为我们在平时会经常用到,当我们在如下的这种方式进行的时候会指向调用它的对象obj;

js 复制代码
let obj={
  name:"aaa",
  foo:function(){
    console.log(this);
  }
}
obj.foo()

// { name: 'aaa', foo: [Function: foo] }

三.new绑定


🦊在讲解new绑定之前,我们先来看下当我们在new一个对象的时候到底做了什么事情。

  1. 创建新的空对象。
  2. 将this指向这个空对象。
  3. 指向函数体中的代码。
  4. 没有显示返回空对象的时候默认返回这个对象。

😶‍🌫️从第二步我们知道,当我们进行new操作的时候会将this绑定到实例化的这个对象上面。

js 复制代码
function foo (name) {
  this.name = name
  console.log(this.name)
}
let bar = new foo("zzz")
console.log(bar)

// zzz
// foo { name: 'zzz' }

通过上述的代码我们可以看到当我们进行对象实例化的时候函数内部的this指向的是实例化的这个对象。

四.this指向绑定事件的元素


js 复制代码
<ul id="color-list">
  <li>red</li>
  <li>yellow</li>
  <li>blue</li>
  <li>green</li>
  <li>black</li>
  <li>white</li>
  </ul>
js 复制代码
// this 是绑定事件的元素
// target 是触发事件的元素 和 srcElememnt 等价
let colorList = document.getElementById("color-list");
colorList.addEventListener("click", function (event) {
  console.log('this:', this);
  console.log('target:', event.target);
  console.log('srcElement:', event.srcElement);
})

🤡有些时候我们会遇到一些困扰,比如在div节点的事件函数内部,有一个局部的 callback 方法,该方法被作为普通函数调用时,callback 内部的this是指向全局对象 window的.

js 复制代码
<div id="div1">我是一个div</div>
js 复制代码
window.id = 'window';
document.getElementById('div1').onclick = function(){
  console.log(this.id); // div1
  const callback = function(){
    console.log(this.id); // 因为是普通函数调用,所以 this 指向 window
  }
  callback();
}

此时有一种简单的解决方案,可以用一个变量保存 div节点的引用,如下:

js 复制代码
window.id = 'window';
document.getElementById('div1').onclick = function(){
  console.log(this.id); // div1
  const that = this; // 保存当前 this 的指向
  const callback = function(){
    console.log(that.id); // div1
  }
  callback();
}

五.显式绑定(call/apply)


🥴有的时候this的指向并不能如我们所愿,这个时候我们需要手动去更改this的指向,来满足我们的需求,其实在JavaScript中给我们提供了能够更改this的绑定,首先我们看下call和apply。

js 复制代码
function foo(name,age){
  console.log(this);
  console.log(name,age);
}
const obj = {
  name:"zs",
  age:12,
}
foo.call(obj,'ls',30)

// { name: 'zs', age: 12 }
// ls 30

通过call函数我们可以看到我们可以手动的将this绑定到我们新定义的对象上面来,并且通过call单个传参的方式将参数传递给了这个函数,实现了函数的调用和this的绑定。

js 复制代码
function foo (name, age) {
  console.log(this)
  console.log(name, age)
}
const obj = {
  name: "zs",
  age: 12,
}
foo.apply(obj, ['nnn', 45])

// { name: 'zs', age: 12 }
// ls 30

我们会发现我们使用apply的方式进行绑定的修改结果依然如此,差别在于他们的传参方式不同,call是单个的方式进行传参的,而apply是通过数组的方式传参的。

六.bind函数的显式绑定


🐻bind的绑定和call和apply的使用有些差别,使用bind会生成一个新的函数,这个新函数我们称之为BF绑定函数,我们需要手动对这个函数进行调用。

js 复制代码
function foo(){
  console.log("foo",this)
}

let obj = {
  name:"why"
}

let bar = foo.bind(obj)
bar()

七.内置函数的调用绑定思考


😂我们在开发中会用到很多内置的函数,比如定时器setTimeout 这个时候我们需要靠经验来判断当前的this指向因为有些东西根本不是我们来调用的,而是函数内部调用的,我们根本不知道他们做了什么,这些内容需要我们自己总结一下,内容如下。

  1. 定时器内部的函数this指向window
js 复制代码
setTimeout(function () {
  console.log(this, "1")
}, 500)
setTimeout(() => {
  console.log(this, "2")
}, 500)
  1. 按钮的点击事件,指向事件发起的对象,也就是绑定事件的元素。
js 复制代码
let box = document.querySelector(".test")
box.onclick = function () {
  console.log(this)
}
  1. forEach中的this也是指向window
js 复制代码
const array = [1, 2, 3, 4, 5, 6]
array.forEach(item => {
  console.log(this)
})

八.绑定优先级的比较


🥴我们前面了解的都是一些独立的规则,但是实际的情况往往是比较复杂的,可能涉及到多个绑定一起使用的情况,这个时候我们就需要研究一下不同函数之间调用的优先级。

  1. 直接调用(默认绑定)的优先级是最低的。
  2. 显式绑定优先级高于对象绑定(隐式绑定)。
js 复制代码
function foo () {
  console.log(this.name)
}
let obj = {
  name: "zzz",
  bar: foo
}
obj.bar.call({
  name: "aaa"
})

// aaa
  1. new绑定优先级高于对象绑定(隐式绑定)的优先级
js 复制代码
function foo (name) {
  this.name = name
  console.log(this.name)
}
let obj = {
  name: "zzz",
  bar: foo
}
new obj.bar("ccc")

// ccc
  1. new绑定不能和call与apply一起使用,new绑定的优先级比bind高。
js 复制代码
function foo (name) {
  this.name = name
  console.log(this.name)
}

let bar = foo.bind("zzz")
new bar("ccc")

// ccc
  1. bind和apply的优先级,bind的优先级更高,也高于call因为call和apply使用方法一样。
js 复制代码
function foo () {
  console.log(this)
}

let bar = foo.bind("zzz")
bar.call("aaa")

// zzz

九.绑定规则之外


🥴其实在上述的绑定规则之外还有许多我们有时候按照规则难以理解的情况,我们来总结下有哪些情况。

  1. 在显式绑定当中如果传入null/undefined这个绑定会被忽略,使用默认规则,严格模式能够绑定。
js 复制代码
function foo () {
  console.log(this)
}

foo.apply(null)
foo.apply(undefined)

// window
// window
  1. 创建一个函数的间接引用,使用函数的默认绑定规则,指向window(了解)
js 复制代码
var obj1 = {
  name:"obj1",
  foo:function(){
    console.log("foo",this)
  }
}
var obj2={
  name:"obj2"
};
(obj2.foo = obj1.foo)()

// window

十.箭头函数的使用


😂我们之前使用函数的方式是这样的。

js 复制代码
// 普通函数方式
function foo1(){}
// 函数表达式方式
let foo2 = function(){}

🐣箭头函数的写法

js 复制代码
// 箭头函数的完整写法
// 1.()函数的参数
// 2.{}函数体
let foo3 = (name,age)=>{
  console.log(name);
  console.log(age);
}

😙箭头函数与普通函数的区别:箭头函数中没有this和arguments并且不能作为构造函数使用。

🤡箭头函数的优化方式:

  1. 当一个参数的时候可以省略函数参数的小括号。
js 复制代码
let name = ["abc","bca","nba"];
name.forEach(item=>{
  console.log(item);
})
  1. 如果函数体中只有一行执行代码{}可以省略,但是一行代码中不能带return;
js 复制代码
let name = ["a","b","c"];
name.filter(item=> console.log(item))
  1. 如果函数体中只有一行代码,那么这行代码的返回值会作为整个函数的返回值。
js 复制代码
let name = ["a","b","c"];
name.filter(item=>item==='a')
  1. 如果默认返回值是一个对象,那么这个对象必须加小括号
js 复制代码
let arr = ()=>({name:"zzz",age:12})

💗箭头函数使用案例:使用所有nums的所有平方和的值。

js 复制代码
let nums = [20,30,11,15,111]
js 复制代码
/*
*1.首先调用filter函数然后返回
*2.然后调用map函数然后返回
*3.然后调用reduce计算后返回。
*/
let nums = [20, 30, 11, 15, 111]
let num = nums.filter(item => item % 2 === 0)
  .map(item => item * item)
  .reduce((pre, cur) => pre + cur, 0)
console.log(num)

// 1300

十一.箭头函数中的this


😁箭头函数中是没有this的,所以this如果在箭头函数中的this就是上级作用域的this。

js 复制代码
let bar =()=>{
  console.log(this)
}
// window

👽因为没有this的原因,箭头函数中使用显式绑定也是无法绑定过去的。

js 复制代码
let bar =()=>{
  console.log(this)
}
bar.call({name:"zzz"})
// window

😭我们再来看下一个案例来明白下this的查找规则。

js 复制代码
let obj = {
  name: "obj",
  foo: function () {
    let bar = () => {
      console.log(this)
    }
    return bar
  }
}
let fn = obj.foo()
fn.call("bbb")

// obj
  1. 首先调用foo函数返回bar定义的函数,箭头函数没有this。
  2. 根据作用域的查找规则,会向上层找this,foo函数中是有作用域的。
  3. 使用call函数是无法更改掉this的。
  4. 所以打印出来this的指向是obj函数。

十二.this常见面试题解析


js 复制代码
oconst o1 = {
  text: 'o1',
  fn: function () {
    return this.text;
  }
}

const o2 = {
  text: 'o2',
  fn: function () {
    return o1.fn();
  }
}

const o3 = {
  text: 'o3',
  fn: function () {
    var fn = o1.fn;
    return fn();
  }
}

console.log(o1.fn()); // o1
console.log(o2.fn()); // o1
console.log(o3.fn()); // undefined

👽解析:首先o1.fn是一个对象调用,所以this指向的是这个对象o1,o2.fn()单调用的时候返回的是o1.fn因此指向的仍然是o1,第三个o1.fn进行了重新赋值然后调用,独立函数调用指向undefined。


js 复制代码
var name = "window";
var person = {
  name:"person",
  sayName:function(){
    console.log(this.name);
  }
}
function sayName(){
  var sss = person.sayName;
  sss(); // window
  person.sayName(); // person
  (person.sayName)(); // person  等价于 person.sayName()
  (b=person.sayName)(); // window
}
sayName();

js 复制代码
var name = 'window'
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()(); //window 独立函数调用
person1.foo3.call(person2)() // window // 默认调用
person1.foo3().call(person2) // person2
person1.foo4()() // person1
person1.foo4.call(person2)() // person2
person1.foo4().call(person2) // person1

js 复制代码
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
person1.foo2.call(person2) // person1
person1.foo3()() // window
person1.foo3.call(perosn2)() // window
person1.foo3().call(person2) // person2
person1.foo4()() // person1
person1.foo4.call(person2)() // person2
person1.foo4().call(person2) // person1

js 复制代码
var name = "window"
function Person(name){
  this.name = name
  this.obj = {
    name:'obj',
    foo: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

相关推荐
brrdg_sefg12 分钟前
Rust 在前端基建中的使用
前端·rust·状态模式
m0_7482309437 分钟前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端·rust·excel
qq_589568101 小时前
Echarts的高级使用,动画,交互api
前端·javascript·echarts
黑客老陈2 小时前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安2 小时前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
暴富的Tdy2 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js
neeef_se2 小时前
Vue中使用a标签下载静态资源文件(比如excel、pdf等),纯前端操作
前端·vue.js·excel
m0_748235612 小时前
web 渗透学习指南——初学者防入狱篇
前端