JS Advance --- this的基本使用

303 阅读13分钟

在常见的编程语言中,几乎都有this这个关键字(Objective-C中使用的是self)

但是JavaScript中的this更加灵活,无论是它出现的位置还是它代表的含义

作用

this可以让我们的代码编写更为的简便

没有this

const info = {
  name: 'Klaus',

  eatting() {
    console.log(`${info.name} eatting`)
  },

  running() {
    console.log(`${info.name} running`)
  },

  playing() {
    console.log(`${info.name} playing`)
  }
}

此时如果,我们将对象的名称修改为preson

const person = {
  name: 'Klaus',

  eatting() {
    console.log(`${person.name} eatting`)
  },

  running() {
    console.log(`${person.name} running`)
  },

  playing() {
    console.log(`${person.name} playing`)
  }
}

此时,所有使用了info的地方都需要修改为person,十分的繁琐。

但是使用了this后,可以更为方便让我们在对象中去引用自身对象

// 使用this
const person = {
  name: 'Klaus',

  eatting() {
    console.log(`${this.name} eatting`)
  },

  running() {
    console.log(`${this.name} running`)
  },

  playing() {
    console.log(`${this.name} playing`)
  }
}

此时如果需要再次修改对象的名称的时候,只要改动一处即可

指向

全局

browser --- 浏览器

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    console.log(this) // => window
  </script>
</body>
</html>

node --- module.exports对象

node在初次加载模块代码的时候,为了避免污染执行环境,会将模块中的对应代码放到一个沙盒中进行运行

而这个沙盒其实就是一个普通的函数(不是一个箭头函数),而且在执行的时候,使用call方法将该函数内部的this修改成了module.exports

所以在模块内部直接打印this会输出{}

在模块内部直接打印arguments会发现其上边默认挂载了一系列的属性和方法

exports.name = 'Klaus'

// 实际上,node会将模块编译为一个IIFE再进行执行
// 执行的时候会使用call方法将模块内全局的this修改为module.exports对象
console.log(this) // => { name: 'Klaus' }

exports.age = 24
function foo() {
  console.log(global === this) // => true
}

// 注意: 仅仅是模块内部 全局的this 指向 module.exports
// 但是函数内部的this的值是 global对象,不是module.exports
foo()

console.log(global === this) // => false
console.log(this) // => {}

函数内部

所有的函数在被调用时,都会创建一个执行上下文

这个执行上下文中,除了存在AO对象,作用域链之外

还存在一个特别的属性 this

注意: 和函数的作用域是在编译的时候就被确定( 编译时绑定 )不同的是

函数的this是动态绑定的,也就是说函数的this的是只有在函数调用的时候才会被确定( 运行时绑定

在函数编译的时候是无法知道this的值到底是哪一个的

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

foo() // => globalThis

const bar = {
  foo
}

bar.foo() // => 调用对象 在这里是bar对象

foo.call('coderwxf') // => 字符串‘coderwxf’所对应的String包装类

绑定规则

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

默认绑定

默认绑定,又被叫做独立函数调用

  • 独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用 或者说,此时调用独立函数的调用者是全局对象
  • 所以独立函数调用的this的值是globalThis
function foo() {
  console.log(this)
}

foo() // => globalThis
const info = {
  foo() {
    console.log(this)
  }
}

const fn = info.foo
fn() // => globalThis
function bar() {
  return function() {
    console.log(this)
  }
}

const fn = bar()

fn() // => globalThis

隐式绑定

所谓的隐式绑定, 其实就是通过某个对象进行调用的, 即作为方法进行调用

const info = {
  name: 'info',
  foo() {
    console.log(this)
  }
}

info.foo() // => info对象
const info = {
  name: 'info',
  foo() {
    console.log(this)
  }
}

const bar = {
  name: 'bar',
  foo: info.foo
}

bar.foo() // => bar对象

显示绑定

通过call,apply, bind方法,显示手动的去修改函数的this指向

call 和 apply
const info = {
  foo() {
    console.log(this)
  }
}

const fn = info.foo

// 直接使用call或apply方法的时候, 函数会被立即执行,此时call和apply方法的功能和函数直接调用是没有任何的区别
// call和apply是定义在Function.prototype上的,所以可以被所有的函数的实例对象所调用
fn() // => globalThis
fn.call() // => globalThis
fn.apply() // => globalThis
const info = {
  foo() {
    console.log(this)
  }
}

const fn = info.foo

fn() // => globalThis
fn.call(info) // => info对象
fn.apply(info) // => info对象
function sum(num1, num2) {
  console.log(this)
  console.log(num1 + num2)
}

// call传递参数的方式是 使用剩余参数 方式进行参数传递
sum.call('call', 10, 20)

// apply传递参数的方式是 将所有参数全局存放于数组中进行参数传递
sum.apply('apply', [10, 20])
bind
function foo() {
  console.log(this)
}

// bind方法和 call/apply的最大区别是
// call/apply --- 修改函数内部this并立即执行
// bind方法 --- 返回一个修正过this的函数,由用户自己去决定函数的调用时机
const fn = foo.bind('Klaus')

// 其实这里是默认调用, 原本的this应为globalThis
// 但是在函数实际调用之前使用bind方法 修正了this
// 当显示绑定和默认绑定同时存在的时候,显示绑定的优先级比默认绑定的优先级高
fn()
function sum(num1, num2) {
  console.log(this)
  console.log(num1 + num2)
}

const fn = sum.bind('foo')

// 参数传递
fn(20, 30)

new绑定

使用new关键字去调用函数的时候,也就是将函数当做构造函数去进行调用的时候,

构造函数中的this就是调用这个构造函数的时候创建的实例对象

使用new关键字来调用函数是,会执行如下的操作:

  1. 创建一个全新的对象(其实就是实例对象)

  2. 对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性

    ( 对象的__proto__的值会被赋值为对象.prototype所对应的值)

  3. 这个新对象会绑定到函数调用的this上

  4. 如果函数没有返回其他对象,表达式会返回这个新对象

function Person(name, age) {
  // 构造函数内部的this其实就是 Person的实例对象,在这个案例中就是后边的per对象
  // 只不过此时打印处理的实例对象是个空对象,因为执行到第4行的时候,还没有进行任何的属性赋值操作
  console.log(this)
  this.name = name
  this.age = age
}

const per = new Person('Klaus', 23)

内置函数中的this

定时器

setTimeout(function() {
  // setTimeout/setInterval中的this的值
  // 1,浏览器中是 window对象
  // 2. node中是 Timeout对象
  console.log(this)
}, 2000)

Dom事件

const btnElem = document.createElement('button')
btnElem.innerHTML = 'click me'
document.body.appendChild(btnElem)


btnElem.onclick = function() {
  console.log(this) // => btnElem对象(触发点击的那个dom对象)
}

btnElem.addEventListener('click', function() {
  console.log(this) // => btnElem对象(触发点击的那个dom对象)
})

数组的高阶函数

// 这里以forEach为例
[1, 2, 3].forEach(function() {
  console.log(this) // => globalThis
})
// 所有的数组的高阶函数都有第二个参数,可以修正第一个函数中的this的值
// 包括但不限于 map/forEach/filter/map/reduce/find/findIndex等
[1, 2, 3].forEach(function() {
  console.log(this) // 如果指定了第二个参数,那么this的值就是第二个参数
}, 'Klaus')

优先级

默认规则的优先级最低

  • 毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定this

显示绑定优先级高于隐式绑定

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

foo() // => globalThis

foo.call('abc') // => String{ 'abc' }
foo.apply('abc') // => String{ 'abc' }

foo.bind('abc')() // => String{ 'abc' }
function foo() {
  console.log(this)
}

// bind 的优先级 高于 call和apply
foo.bind('Klaus').call('abc') // => String{ 'Klaus }
foo.bind('Klaus').apply('abc') // => String{ 'Klaus }

new绑定优先级高于隐式绑定

const info = {
  foo: function() {
    console.log(this)
  }
}

const f = new info.foo() // => foo对象
const info = {
  foo() {
    console.log(this)
  }
}

const f = new info.foo() // => error 如果对象的属性的值是函数,且使用了ES6提供的简写的方式的时候
// 在编译的时候V8是知道foo函数是作为info的方法,而不是作为构造器去使用的,所以此时去new info.foo 是会报错的

new绑定优先级高于bind

new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高

所以这里使用new关键字和bind方法来进行比较

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

const newFoo = foo.bind('abc')

const f = new newFoo() // => foo对象
function foo() {
  console.log(this)
}

// V8检测的时候发现你使用bind修正过this了
// 那么修正过的this后返回的函数,多数情况下是作为函数去进行调用的
// 所以此时就会报错
const f = new foo.bind('abc')() // => 报错

new > 显示绑定(apply/call/bind) > 隐式绑定(作为方法调用) > 默认绑定(直接调用)

规则之外

忽略显示绑定

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

// 手动指定this的时候,如果this的值设置为null或undefined的时候
// 会自动忽略所设置的this,而采用默认的this指向
foo.apply(undefined) // => globalThis
foo.call(undefined) // => globalThis

foo.apply(null) // => globalThis
foo.call(null) // => globalThis

foo.bind(null)() // => globalThis
foo.bind(undefined)() // => globalThis

// 如果每一行的代码不以分号结尾的时候 且代码以括号开头(包括大括号,小括号,中括号)的时候
// 为了避免V8词法解析的时候无法准确区分每行代码的开始和结束,而报奇怪的错误
// 需要在当前代码的开头或上一行代码的结尾处加上分号,以将两行代码进行有效且显示的区分
;[1, 2, 3].forEach(function() {
  console.log(this) // => globalThis
}, null)

;[1, 2, 3].forEach(function() {
  console.log(this) // => globalThis
}, undefined)

间接函数引用

const bar = {
  name: 'bar',
  foo: function() {
    console.log(this)
  }
}

const baz = {
  name: 'baz'
}

// 返回的结果是bar.foo所指向的那个值,所以实际输出的结果为foo函数
console.log(baz.foo = bar.foo) // => foo函数对象

// 下述代码的实际等价于 const foo = bar.foo; foo();
// 所以输出的结果为globalThis
;(baz.foo = bar.foo)() // => globalThis

箭头函数

箭头函数是ES6之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁:

  • 箭头函数不会绑定this、arguments属性
    • 如果在箭头函数内部使用了this,那么会沿着作用域链去上层作用域中寻找对应的this的值
  • 箭头函数没有显示原型对象,所以不能作为构造函数来使用
    • 也就是说箭头函数,不能和new一起来使用,会抛出错误
箭头函数的使用

基本使用

const foo = () => {
  console.log('foo') // => 'foo'
}

foo()

简写

  • 如果只有一个参数()可以省略
const foo = v => {
  console.log(v) // => 'foo'
}

foo('foo')
  • 如果函数执行体中只有一行代码, 那么可以省略大括号, 并且这行代码的返回值会作为整个函数的返回值
console.log([1, 2, 3, 4].filter(item => item % 2 === 0)) // => [2, 4]
  • 如果函数执行体只有返回一个对象, 那么需要给这个对象加上(),以声明返回的结果是一个整体
const foo = () => ({
  name: 'Klaus',
  age: 23
})

console.log(foo())
箭头函数中this的获取

箭头函数不使用this的四种标准规则(也就是不绑定this),而是根据外层作用域来决定this

不使用箭头函数

const foo = {
  data: [],

  fetchData() {
    // 常见的保存this的变量的命名为 _this, that, self等
    var _this = this

    setTimeout(function() {
      _this.data = [1, 2, 3, 4]
    }, 2000)
  }
}

foo.fetchData()

使用箭头函数后

const foo = {
  data: [],

  fetchData() {
    setTimeout(() => data = [1, 2, 3, 4], 2000)
  }
}

foo.fetchData()

阶段练习

var name = "window";

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

function sayName() {
  var sss = person.sayName;
  sss(); // => globalThis
  
  person.sayName(); // => person

  // V8在编译的时候会主动移除第一个括号 (person.sayName)() 等价于 person.sayName()
  (person.sayName)(); // => person
  
  (b = person.sayName)(); // => globalThis
}

sayName();
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.foo()中的上层作用域是全局执行上下文
person1.foo2(); // => window
// 箭头函数内部无this, 所以不管怎么显示绑定,实际调用的时候都会忽略
person1.foo2.call(person2); // => window

person1.foo3()(); // => window
person1.foo3.call(person2)(); // => window
person1.foo3().call(person2); // => person2

// person1.foo4函数调用后返回的箭头函数的上层作用域是person1.foo4,而不是全局作用域
person1.foo4()(); // => person1
person1.foo4.call(person2)(); // => person2
person1.foo4().call(person2); // => person1
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)
    }
  }
}

// 每调用一次构造函数,会生成一个新的实例对象
// 所以每次调用构造函数的时候,其内部的this的值是不一样的
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1() // => person1
person1.foo1.call(person2) // => person2

// person1.foo2是箭头函数,所以会去上层作用域寻找对应的this
// js的作用域是在函数编译的时候就已经被确定
// 所以person1.foo2的this,实际上是Person的实例对象,在这里就是person1
// 简而言之就是函数是有自己的函数作用域的,不管这个函数是普通函数 还是 箭头函数
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'

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

// foo2函数调用后返回的结果是一个箭头函数,内部没有this
// 所该函数返回的监听函数在调用后如果需要在内部使用this
// 就会去其上一层(在这里就是foo2函数)中进行查找
// 而在下一行代码中,foo2是通过person1.obj来进行调用的
// 所以foo2中的this此时就是 person1.obj对象
person1.obj.foo2()() // => person1.obj
person1.obj.foo2.call(person2)() // => person2
person1.obj.foo2().call(person2) // => person1.obj

apply, call, bind的简单模拟

apply, call, bind 方法实际中使用C++在浏览器中进行了对应的实现

这里仅仅只是使用js对其中的逻辑结构进行了模拟

Object构造函数

const str = 'Klaus'
const num = 123
const obj = { name: 'Alex' }

console.log(new Object(str)) // => String{ 'Klaus' }
console.log(new Object(num)) // => Number { 123 }
console.log(new Object(obj)) // => { name: 'Alex' }

console.log(new Object()) // => {}
console.log(new Object({})) // => {}
console.log(new Object(null)) // => {}
console.log(new Object(undefined)) // => {}

剩余参数

剩余参数( rest params ) 可以让我们在接收函数参数的时候,可以方便的接收任意个数的参数

function fn(...args) {
  // 所有的剩余参数会被搜集到一个数组中
  // 如果没有任何的参数,args的值是空数组
  console.log(args)
}

fn(1) // => [1]
fn(1, 2) // => [1, 2]
fn(1, 2, 3) // => [1, 2, 3]
fn() // => []

展开运算符

展开运算符(spread operation)可以方便我们将参数进行解构后传入另一个地方进行使用

// 浅拷贝
const arr = [1, 2, 3]

const newArr = [...arr]

console.log(newArr)
const arr = [1, 2, 3]

function sum(num1, num2, num3) {
  return num1 + num2 + num3
}

console.log(sum(...arr))

模拟call

// 在函数的原型上挂载自定义的call方法
Function.prototype.customCall = function(thisArg, ...args) {
  thisArg = ![null, undefined].includes(thisArg) ? new Object(thisArg) : globalThis

  // 避免覆盖传入的this上本身存在的函数,所以在挂载函数的时候,函数名使用Symbol来创建一个独一无二的函数名
  const fnSymbol = Symbol('fn')
  
  // 为实际的this绑定上实际调用的函数
  thisArg[fnSymbol] = this

  return thisArg[fnSymbol](...args)
}

const info = {
  name: 'Klaus'
}

function sum(num1, num2) {
  console.log(this)
  return num1 + num2
}

// 测试代码

// 1. 只设置this,但是不传参数
console.log(sum.customCall(123))

// this为对象
console.log(sum.customCall(info, 10, 20))

// this为基本数据类型
console.log(sum.customCall('Klaus', 10, 20))

// this设置为null或undefined的时候,因为globalThis对象
console.log(sum.customCall(null, 10, 20))

模拟apply

// 在函数的原型上挂载自定义的apply方法
// 需要为arg设置默认值,以防止没有参数的时候,出现 ...undefined的情况,从而报错
Function.prototype.customApply = function(thisArg, args = []) {
  thisArg = ![null, undefined].includes(thisArg) ? new Object(thisArg) : globalThis

  const fnSymbol = Symbol('fn')
   
  thisArg[fnSymbol] = this

  return thisArg[fnSymbol](...args)
}


const info = {
  name: 'Klaus'
}

function sum(num1, num2) {
  console.log(this)
  return num1 + num2
}

// 测试代码
console.log(sum.customApply(123))
console.log(sum.customApply(info, [10, 20]))

模拟bind

bind传递参数的几种方式

// 方式1
function sum(num1, num2, num3, num4) {
  return num1 + num2 + num3 + num4
}

const newSum = sum.bind('Klaus')
console.log(newSum(1, 3, 4, 5))
// 方式2
function sum(num1, num2, num3, num4) {
  return num1 + num2 + num3 + num4
}

const newSum = sum.bind('Klaus', 1, 3, 4, 5)
console.log(newSum())
// 方式三
function sum(num1, num2, num3, num4) {
  return num1 + num2 + num3 + num4
}

const newSum = sum.bind('Klaus', 1, 3)
console.log(newSum(4, 5))

模拟实现

Function.prototype.customBind = function(thisArg, ...bindArgs) {
 thisArg = ![null, undefined].includes(thisArg) ? new Object(thisArg) : globalThis

 const fnSymbol = Symbol('fn')

 thisArg[fnSymbol] = this

 return function(...args) {
   return thisArg[fnSymbol](...bindArgs, ...args)
 }
}

// test code
function sum(num1, num2, num3, num4) {
  console.log(this)
  return num1 + num2 + num3 + num4
}

const newSum = sum.customBind('Klaus')
console.log(newSum(2, 3, 4, 5))