JavaScript中的this绑定规则

711 阅读6分钟

this指向

this是动态绑定的,与定义的位置没有关系,与调用的方式有关系,主要有四个规则:

  1. 默认绑定
  2. 隐式绑定
  3. 显示绑定
  4. new绑定

this指向的作用

this指向可以让我们不用频繁的修改函数、对象里面的数据

let obj = {
  name: "zs",
  eating: function () {
    console.log(this.name + '吃');
  },
  running: function () {
    console.log(this.name + '跑');
  },
  studying: function () {
    console.log(this.name + '学习');
  }
}

obj.eating()
obj.running()
obj.studying()

全局作用域

node环境下没有window 而且this指向{},浏览器中this指向window

为什么node指向{}呢?

因为node中会按模块加载js文件,然后编译放到一个函数中,这个函数中有一个call(thisValue,...)函数,nodecall({})函数里绑定了{}

默认绑定

什么情况下是默认绑定呢?独立函数调用的时候

  • 独立函数调用我们可以理解为函数没有绑定到某个对象上进行调用
  • 可以通过下面的案例来理解
// 1.案例一
function foo() {
  console.log(this);
}
foo()

// 2.案例二
function foo1() {
  console.log(this);
}

function foo2() {
  console.log(this);
  foo1()
}

function foo3() {
  console.log(this);
  foo2()
}

foo3()

// 3.案例三
var obj = {
  name: 'why',
  foo: function () {
    console.log(this);
  }
}

var bar = obj.foo
bar()

// 4.案例四
function foo() {
  console.log(this);
}
var obj = {
  name: 'why',
  foo: foo
}

var bar = obj.foo
bar() 

// 5.案例五
function foo() {
  function bar() {
    console.log(this);
  }
  return bar
}

var fn = foo()
fn()

var obj = {
  name: 'zs',
  eating: fn
}

obj.eating()  // 隐式绑定

隐式绑定

调用方式是通过某个对象进行调用:

  • 也就是它的调用位置中,是通过某个对象发起的函数调用
  • object对象会被js引擎绑定给函数中的this
// 1.案例一
function foo() {
  console.log(this);
}

var obj = {
  name: 'zs',
  foo: foo
}

obj.foo()

// 2.案例二
var obj = {
  name: 'zs',
  eating: function () {
    console.log(this.name + '在吃东西');
  },
  running: function () {
    console.log(this.name + '在跑步');
  }
}

obj.eating()
obj.running()

var fn = obj.eating()
fn()

// 3.案例三
var obj1 = {
  name: 'obj1',
  foo: function () {
    console.log(this);
  }
}

var obj2 = {
  name: 'obj2',
  bar: obj1.foo
}

obj.bar()

(person.sayName)()	// 这种也是隐式调用哦

显示绑定

apply函数

apply 函数指定一个对象的执行函数,改变函数中的 this

var obj = {
  uname: '张三'
}
var fun1 = function (a, b) {
  console.log(a + b);
  console.log(this);
}
func.apply(obj, [1, 2])  // this 指向 obj 对象
func.apply('aaa') // this 指向 aaa

call函数

func.call(obj, 1, 2)  // this 指向 obj 对象

bind函数

bind 函数会改变原始函数的调用者,但是不会执行函数,所以 func.bind(obj,1,2) 不会被调用,只返回一个新的函数

function foo() {
  console.log('函数被调用了', this)
}

var newFoo = foo.bind('aaa')

newFoo() // 显示绑定的优先级大于默认绑定 所以this指向aaa

异同

相同点:都可以改变 this 指向

不同点:

  • call()bind() 的传参为参数列表的形式,apply() 的传参为参数数组
  • call()apply() 改变 this 指向的时候会执行函数,bind() 不执行函数,只返回一个新的函数

new绑定

JS中的函数可以当作一个类的构造函数来使用,也是使用new关键字

使用new关键字来调用函数会执行以下操作:

  1. 创建一个全新的对象
  2. 这个新对象会被执行prototype连接
  3. 这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成)
  4. 如果函数额米有返回其他对象,表达式会返回这个新对象
function Person(name, age) {
  this.name = name
  this.age = age
}

// 当我们调用 new 的时候会自动给我们生成一个新的对象p1={ name:'zs',age:18 },而我们的this就指向这个对象
var p1 = new Person('zs', 18)
// var p1 = { name: 'zs', age: 18 }
console.log(p1.name, p1.age);

var p2 = new Person('ls', 30)
console.log(p2.name, p2.age);

var obj = {
  foo: function () {
    console.log(this);
  }
}

new obj.foo() // new绑定与obj绑定冲突

其他this指向

// 1.setTimeout
function mySetTimeout(fn, duration) {
  // 独立函数调用 window 系统的setTimeout应该使用的是默认绑定,但是如果传入的是箭头函数的时候就不一样了
  fn()
  // 显示绑定 aaa
  // fn.call('aaa')
}

mySetTimeout(function () { }, 3000)

setTimeout(function () {
  console.log(this);
}, 3000)

// 2. 监听点击
const boxDiv = document.querySelector('.box')
// 第一种监听
boxDiv.onclick = function () {
  console.log(this); // 返回了 <div class="box"></div>
  // 可见它内部其实是使用了隐式绑定 boxDiv.onclick
}

// 第二种监听
boxDiv.addEventListener('click', function () {
  // 这种监听可以调用很多次,可见它内部是拿到了我们的函数使用call这种显示绑定 call(boxDix)
  console.log(this);
})
boxDiv.addEventListener('click', function () {
  console.log(this);
})
boxDiv.addEventListener('click', function () {
  console.log(this);
})

// 3.数组 forEach/map/filter/find
var names = ['abc', 'cba', 'nba']
names.forEach(function (item) {
  console.log(item, this);
}, 'abc')
names.map(function (item) {
  console.log(this);
}, 'map')

优先级

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

// new关键字不能和apply/call一起来使用
function foo() {
  console.log(this);
}

var bar = foo.bind('aaa')

var obj = new bar()

特殊绑定

function foo() {
  console.log(this);
}

// 当绑定null/undefined的时候 this指向window
foo.apply(null)
foo.apply(undefined)
foo.call(null)
foo.call(undefined)
foo.bind(null)
foo.bind(undefined)

var obj1 = {
  names: 'obj1',
  foo: function () {
    console.log(this);
  }
}

var obj2 = {
  name: 'obj2'
};

(obj2.bar = obj1.foo)() // 间接函数引用,有赋值表达式,这样就会被当作为默认绑定
(person.sayName)()	// 隐式调用

箭头函数:this指向上级作用域,不绑定this(就是前面的规则没有用)

// 1.箭头函数
var name = 'zs'

var foo = () => {
  console.log(this);
}

foo()
var obj = { foo: foo }
obj.foo()
foo.call('abc')

// 2.引用场景
var obj = {
  data: [],
  getData: function () {
    // 发生网络请求 将结果放到data属性中
    setTimeout(function () {
      var result = ['abc', 'cba', 'nba']
      // 但是setTimeout中的this是默认绑定的,指向window,所以是不能这样做,那我们怎么办呢?
      this.data = result
      console.log(obj.data);
    }, 2000)

    // 第一种解决方法,在箭头函数没有之前的方案
    var _this = this
    setTimeout(function () {
      var result = ['abc', 'cba', 'nba']
      _this.data = result
      console.log(obj.data);
    }, 2000)

    // 第二种解决方案 箭头函数
    setTimeout(() => {
      var result = ['abc', 'cba', 'nba']
      this.data = result
      console.log(obj.data);
    }, 2000)
  }
}

obj.getData()

实现call、apply、bind

call

Function.prototype.mycall = function (thisArg, ...args) {
  // 在这里可以去指向调用的那个函数
  // 问题是我们要获取到到底是哪一个函数执行了mycall
  // 1.获取需要被执行的函数
  var fn = this

  // 2.对thisArg转成对应的对象类型(Object可以转成对应的对象类型,如果是数字就是number,字符串就是string)
  thisArg = thisArg ? Object(thisArg) : window

  // 隐式转换
  thisArg.fn = fn
  // 3.调用需要被执行的函数
  var result = thisArg.fn(...args)
  delete thisArg.fn

  // 4.将最终的结果返回
  return result
}

function foo() {
  console.log('foo被调用', this);
}

function sum(num1, num2) {
  console.log('sum被调用', this, num1, num2);
  return num1 + num2
}

// 默认进行隐式绑定
foo.mycall('abc', 10, 20)
var result = sum.mycall(123, 10, 20)
console.log(result);

apply

Function.prototype.myapply = function (thisArg, argArray) {
  // 1.获取到要执行的函数
  var fn = this

  // 2.判断thisArg的类型 0比较特殊 它算作没有值false 所以我们得优化
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window

  // 3.处理绑定的thisArg
  thisArg.fn = fn

  var result
  // if (!argArray) {
  //   result = thisArg.fn()
  // } else {
  //   result = thisArg.fn(...argArray)
  // }

  // argArray = argArray ? argArray : []

  argArray = argArray || []
  result = thisArg.fn(...argArray)

  // 4.删除thisArg
  delete thisArg.fn

  // 5.返回值
  return result
}

function sum(num1, num2) {
  console.log('sum被调用', this, num1, num2);
  return num1 + num2
}

function foo(num) {
  return num
}

function bar() {
  console.log("bar被调用", this);
}

// 系统调用
// var result = sum.apply('abc', [10, 20])
// console.log(result);

// 自己实现
// var result = sum.myapply('abc', [10, 20])
// console.log(result);
// var result2 = foo.myapply('abc', [20])
// console.log(result);
// bar.myapply('abc')

// 0比较特殊 它算作没有值false
bar.myapply(0)

bind

Function.prototype.mybind = function (thisArg, ...argArray) {
  // 1.获取到真实需要调用的数据
  var fn = this

  // 2.绑定this
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window

  function proxyFn(...args) {
    // 隐式绑定
    // 3.将函数放到thisArg中进行调用
    thisArg.fn = fn
    // 对两个传入的参数进行合并
    var finalArgs = [...argArray, ...args]
    var result = thisArg.fn(...finalArgs)
    delete thisArg.fn

    // 4.返回结果
    return result
  }

  return proxyFn
}

function foo() {
  console.log('foo被调用', this);
}

function sum(num1, num2, num3, num4) {
  console.log(num1, num2, num3, num4);
}

// 系统bind的使用
// var bar = foo.bind('abc')
// bar()

// var newSum = sum.bind('aaa', 10, 20, 30, 40)
// newSum()
// var newSum = sum.bind('aaa')
// newSum(10, 20, 30, 40)
// var newSum = sum.bind('aaa', 10, 20)
// newSum(30, 40)

// 使用自定义bind
// var bar = foo.mybind('abc')
// var result = bar()
// console.log(result);

var newSum = sum.mybind('abc', 10, 20)
newSum(30, 40)