五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解

53 阅读9分钟

this绑定的五种场景

默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定

绑定一:默认绑定;

绑定二:隐式绑定;

绑定三:显示绑定;

绑定四:new绑定;

一.默认绑定

在独立函数调用情况下发生:理解成函数没有被绑定到某个对象上进行调用或者说全局函数绑定在window对象上进行调用

// "use strict"

    // 定义函数
    // 1.普通的函数被独立的调用
    function foo() {
      console.log("foo:", this)   //this指向 windows对象
    }
    foo()

    // 2.函数定义在对象中, 但是独立调用
    var obj = {
      name: "hhh",
      bar: function() {
        console.log("bar:", this) 
      }
    }
    var baz = obj.bar//类似函数定义中的赋值表达式
    baz()    //this指向 windows对象

    // 3.高阶函数
    function test(fn) {
      fn()
    }
    test(obj.bar)    //this指向 windows对象

    // 4.严格模式下, 独立调用的函数中的this指向的是undefined

二. 隐式绑定

隐式绑定有一个前提条件:

this所在的函数必须是调用对象的属性,通过引用间接的将this绑定到了这个对象上;

// 隐式绑定
    function foo() {
      console.log("foo函数:", this)
    }
    var obj = {
      bar: foo
    }
    obj.bar()  //foo函数: Object

三. 显式绑定

就是对象内部不包含函数的引用,但要在这个对象上强制调用(更改this指向), JavaScript所有函数都可以使用call和apply方法,明确的绑定了this指向的对象,称之为显式绑定

第一个参数为绑定对象

要求传入一个对象;这个对象的作用是给this准备的。

在调用这个函数时,会将this绑定到这个传入的对象上。

第二个参数,apply为数组,call为参数列表上面的过程

 // 显式绑定
    var obj = {
      name: "hhh"
    }

    function foo() {
      console.log("foo函数:", this)
    }
    
//apply,强制this就是obj对象
 foo.apply(obj)           //foo函数: Object
 foo.apply(obj,[1,2])           //foo函数: Object  ,以数组作为函数调用参数:1 2
 
 //call,强制this就是obj对象
    foo.call(obj)		//foo函数: Object
    foo.call(obj,1,3)		//foo函数: Object ,以参数序列作为函数调用参数:1 2
    
 //bind,强制this就是obj对象    
    var obj = { name: "hhh" }
    var bar = foo.bind(obj)  // this绑定 obj  
    bar()     // foo: {name: 'hhh'}
     function foo(name, age, height, address) {
      console.log("foo:", this)
      console.log("参数:", name, age, height, address)
    }
    var obj = { name: "hhh" }
    var bar = foo.bind(obj, "kobe", 18, 1.88)
    bar("james")	//foo: {name: 'hhh'}	,以参数序列作为函数调用参数:kobe 18 1.88 

四. new绑定

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

那new函数究竟做啥,大致分为三步:

以构造器的prototype属性为原型,创建新对象;

将this(可以理解为上句创建的新对象)和调用参数传给构造器,执行函数体;

如果构造器没有手动返回对象,则返回第一步创建的对象

  function foo() {
      this.name = "hhh"   //this的执行上下文是new的新对象
      console.log("foo函数:", this)
    }

    new foo()    //foo函数: foo      属于object类型

五. 箭头函数

箭头函数是ES6增加的编写函数的方法,比函数表达式更简洁:

箭头函数不会绑定this、arguments属性;

箭头函数不能作为构造函数来使用(不能和new一起来使用,会抛出错误);

箭头函数编写: ()=>{}

 // function写法
  var foo1 = function(name, age) {
      console.log("函数体代码", this, arguments)
      console.log(name, age)
    }

    // 2.箭头函数完整写法
    var foo2 = (name, age) => {
      console.log("箭头函数的函数体")
      console.log(name, age)
    }

箭头函数的编写优化

优化一:如果只有一个参数()可以省略

// 1.优化一: 如果箭头函数只有一个参数, 那么()可以省略
    names.forEach(item => {
      console.log(item)
    })
    var newNums = nums.filter(item => {
      return item % 2 === 0
    })

优化二:函数执行体中只有一行代码, 可以省略大括号{} 和return, 并且这行代码会作为整个函数的返回值

names.forEach( item => console.log(item) )
var newNums = nums.filter(item => item % 2 === 0)

优化三: 如果函数执行体只返回一个对象, 那么需要给这个对象加上(),确保{}归属于对象

var arrFn = () => {} // 注意: 这里是{}执行体
var arrFn = () => ( { name: "xxx" } )

ES6箭头函数(this规则之外)

箭头函数不使用this的四种标准规则(也就是不绑定this),而是根据外层作用域来决定this。 箭头函数并不绑定this对象,那么this引用就会从上层作用于中找到对应的this

  // 2.箭头函数中, 压根没有this
    var bar = () => {
      console.log("bar:", this)
    }
    bar()       //bar: Window {window: Window, self: Window, document: document, name: '', location: Location, …}

六. 内置函数

有时候,会调用一些JavaScript的内置函数(类似windows对象本身拥有的函数),或者一些第三方库中的内置函数。

这些内置函数会要求传入另外一个函数;

JavaScript内部或者第三方库内部会帮助执行;

这些函数中的this又是如何绑定?一般是看相关文档解释,或者根据经验

比如setTimeout、数组的forEach、div的点击

<body>
  
  <button>按钮</button>

  <script>
    // 内置函数(第三方库): 根据一些经验
    // 1.定时器
     setTimeout(function() {
       console.log("定时器函数:", this)      //window
    }, 1000)

    // 2.按钮的点击监听
    var btnEl = document.querySelector("button")
    btnEl.onclick = function() {
       console.log("btn的点击:", this)     //btnEl
    }

    // btnEl.addEventListener("click", function() {
    //   console.log("btn的点击:", this)   //btnEl
    // })

    // // 3.forEach
    var names = ["abc", "cba", "nba"]
    names.forEach(function(item) {
      console.log("forEach:", this)		 //String {'aaaa'}
    }, "aaaa")

  </script>

</body>

七. this绑定规则优先级

显式绑定 > 隐式绑定 > 默认绑定

new绑定 > 隐式绑定 > 默认绑定

new绑定优先级高于bind,但是new绑定和call、apply是不允许同时使用的,所以没有优先级高低

箭头函数独立于其他四种绑定,但是优先级与new一致

八. 面试题

面试题一

var name = "window";

var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};

function sayName() {
  var sss = person.sayName; //通过赋值,使用函数

  sss(); // 独立函数调用, 默认绑定, window -> window

  person.sayName(); // 调用对象内部的函数: 隐式绑定, person -> person

  (person.sayName)(); // 别疑惑哈,跟上面一样,加了括号而已: 隐式绑定, person -> person

  (b = person.sayName)(); // 术语: 间接函数引用(本质走的逻辑就是与sss()一致!), window -> window
}

sayName();

面试题二

var name = 'window'   //windows全局对象变量 name
// {} -> 对象
// {} -> 代码块
var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)  
  },
  foo2: () => console.log(this.name),   //不绑定this,往上层作用域找,这里是windows
  foo3: function () {
    return function () {    //在函数内部,返回一个function函数
      console.log(this.name)
    }
  },
  foo4: function () {  
    return () => {		//在函数内部,返回一个箭头函数  ,不绑定this
      console.log(this.name)
    }
  }
}
var person2 = { name: 'person2' }

//题目:
person1.foo1(); // 进入foo1函数,this绑定自身对象person1
person1.foo1.call(person2); // 进入foo1对象,绑定person2,this指向person2

person1.foo2(); //进入foo2函数,箭头函数不绑定foo2的person1,寻找上层作用域windows
//虽然调用对象内部函数,但这个对象是箭头函数不绑定this。寻找 上层作用域: window
person1.foo2.call(person2);//进入foo2函数,foo2绑定person,箭头函数不绑定foo2的person2,寻找上层作用域windows

person1.foo3()();//进入foo3函数,调用,抛出(返回)的function函数【理解为抛出到windows】
//独立函数调用,在函数内部被返回的函数。默认绑定: window
person1.foo3.call(person2)(); //进入foo3函数,foo3绑定person2,调用,抛出的function函数到windows
person1.foo3().call(person2); //进入foo3函数,foo3绑定person2,抛出的函数未被调用,所以this仍在foo3的作用域,this绑定person2

person1.foo4()();//进入foo4函数,调用,抛出(返回)的箭头函数,不绑定this(理解为this不指向windows),所以this寻找上层作用域,也就是foo4绑定的person1
person1.foo4.call(person2)(); //进入foo4函数,foo4绑定person2,;调用,抛出(返回)的箭头函数,不绑定this(理解为this不指向windows),所以this寻找上层作用域,也就是foo4绑定的person2
person1.foo4().call(person2);  //进入foo4函数,foo4绑定person2,函数内部抛出的箭头函数未被调用,所以在foo4的作用域运行,但是this不被foo4绑定,寻找上层作用域,也就是person1

面试题三

var name = 'window'   

/*
  1.创建一个空的对象
  2.将这个空的对象赋值给this
  3.执行函数体中代码
  4.将这个新的对象默认返回
*/
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)
    }
  }
}

// person1/person都是对象(实例instance)
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(person2)() // 默认绑定: window
person1.foo3().call(person2) // 显式绑定: person2

person1.foo4()() // 上层作用域查找: person1(隐式绑定)
person1.foo4.call(person2)() //  上层作用域查找: person2(显式绑定)
person1.foo4().call(person2) // 上层作用域查找: person1(隐式绑定)


面试题四

var name = 'window'

/*  构造函数。
  1.创建一个空的对象
  2.将这个空的对象赋值给this
  3.执行函数体中代码
  4.将这个新的对象默认返回
*/
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()()   //进入foo1函数,foo1本身绑定person1;调用,抛出的function函数到windows
// 默认绑定: window
person1.obj.foo1.call(person2)() //进入foo1函数,foo1本身绑定person2;调用,抛出的function函数到windows
// 默认绑定: window
person1.obj.foo1().call(person2) //进入foo1函数,foo1本身绑定person2;被抛出的function函数未被调用,所以this不绑定windows,而是在foo1作用域,所以this绑定person2
// 显式绑定: person2

person1.obj.foo2()() //进入foo2函数,foo2本身绑定person1;调用,抛出的箭头函数到windows,但是this不绑定windows,而是寻找上层作用域,也就是foo2,所以This绑定perison1
// 上层作用域查找: obj(隐式绑定)
person1.obj.foo2.call(person2)() //进入foo2函数,foo2本身绑定person2;调用,抛出的箭头函数到windows。但是this不绑定windows,而是寻找上层作用域,也就是foo2,所以This绑定perison2
// 上层作用域查找: person2(显式绑定)
person1.obj.foo2().call(person2) //进入foo2函数,foo2本身绑定person2;抛出的箭头函数到windows,未被调用,所以箭头函数的this作用在foo2,但是不绑定foo2,而是寻找上层作用域person1
// 上层作用域查找: obj(隐式绑定)