谈谈js中的this

164 阅读7分钟

前言

在 JavaScript 中,this 关键字是一个非常重要且容易让人困惑的概念,它的值在不同的执行上下文中会有所不同,很多初学者(包括小管),刚开始接触的时候总会一脸懵,今天我们就来简单谈谈this 的“前世今生”

为什么要有this

提到this,我们必须要知道为什么要有this。我们先来看一段代码。

function identify(context){
    return context.name.toUpperCase()
}

function speak(context){
    var greetting = 'Hello, I am '+ identify(context)
    console.log(greetting)
}
var me ={
    name:'Tom'
}
speak(me)

在这段代码里面identifyspeak函数都得携带一个context参数才能正常运行,当我们写了更多的需要携带参数的函数,就需要携带更多的参数,越写越混乱,而this存在的意义就是可以自动地引用合适的上下文对象。不需要再显式地携带参数。

this

我们用一句话概括this:this 是一个代词,在js中永远代指某一个域,且this只存在于域中时,才有意义。

tip: js中的作用域有: 1.全局作用域 (this在全局下指向的是window) 2.函数作用域 3.块级作用域

this的指向

1. 默认绑定:

一句话先概况:

默认绑定:当函数是独立调用时,this指向的是window

话不多说,先上代码

function foo(){
    let person ={
        name :'熊',
        age : 6
    }
    console.log(this);
}
    foo()

很明显,这里的this存在于foo这个函数的作用域里,是foo的。那我们来执行这段代码

问:这段代码的执行结果是什么?(小管:我猜是执行结果是foo)

答:

image.png 很明显,猜错了,这里的this指向的就是node环境中的全局。也就是说,这个this指向了全局。

为什么呢?继续上代码


function foo(){
    let person ={
        name:'小管',
        age:19
    }
    console.log(this);
}
function bar(){
    let person ={
        name :'熊',
        age : 3
    }
      foo()
}
 bar()

我们让foo这个函数在bar函数中调用,此时的执行结果会是什么呢?

image.png 不好意思,还是全局。

我们又要问为什么了,这里就不得不先提到一个概念:函数的独立调用和非独立调用。

function bar(){
    console.log(this)
}
function foo(){
    console.log(this)
}
const obj ={
    a:1,
    foo:foo
}
obj.foo()
bar()
  • 独立调用:函数作为单独个体直接被调用,不依赖于某个特定对象,例如bar() 。
  • 非独立调用:函数作为对象的方法被调用,通过对象来触发,如obj.foo() 。我们可以把函数看为一个小孩子,当“小孩子”被“家长”拉着调用,就叫非独立调用。

结合上面三个例子,我们发现,当函数被独立调用而不以对象方法的形式调用时,this就指向了window。没错,这就是默认绑定

2.隐式绑定

隐式绑定:当函数的引用有上下文对象时(当函数被一个对象拥有且调用时),this指向该上下文对象

什么意思呢,我们来看一段代码

function foo(){
    console.log(this)
}
const obj ={
    a:1,
    foo:foo
}
obj.foo()

这里的foo函数作为obj中的对象调用,所以foo中的this就指向调用fooobj。执行结果如下

image.png 再来一个

function foo(){
    console.log(this)
}
const obj ={
    a:1,
    foo:foo
}
const obj2 ={
    a:2,
    obj:obj
}
obj2.obj.foo()

这里到底是obj2调用的foo还是obj调用的foo呢?

一句话:就近原则!也就是说这里的foo就是被obj调用了,this也就指向obj

执行结果:

image.png 官方给这种情况取了一个名字:隐式丢失

隐式丢失:当函数的引用有一连串的上下文对象,this指向最近的那个对象。

3.显式绑定

显式绑定call() apply() bind() 显示的将函数的this绑定到一个对象上

看一段代码:

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

想让foo绑定到obj上,但是又不想在obj中加上foo,我们就可以用call() apply() bind()方法绑定,方法如下

  • call()
function foo(){
    console.log(this)
}
var obj = {
    a:1
}

foo.call(obj)

这里的call()是在foo()上的方法,挂载在Function.prototype上,且第八行代码是call()来调用foo()

foo()传入参数,就需要把参数传入call(),如下

function foo(x,y){
    console.log(this.a,x+y)
}

var obj = {
    a:1
}
foo.call(obj,2,3)
  • apply()call()的唯一区别就是传参的时候传入的是数组,如下
function foo(x,y){
    console.log(this.a,x+y)
}

var obj = {
    a:1
}
foo.apply(obj,[2,3])

  • bind() bind()一定会返回一个函数体,必须将返回出的函数调用,且它收到参数时是零散的接收的,参数可以放在bind()上,也可以放在返回出来的函数体上(注意:若bind()和返回的函数体都携带参数时,先用bind()上携带的参数,再用返回额函数体上携带的参数)
function foo(x,y){
   console.log(this.a,x+y)
}

var obj = {
   a:1
}
let bar = foo,bind(1)
bar(3)//打印出1,4
3.new绑定

我们先来聊聊new:

我们来创建一个函数

function Person(){
    this.name ='小管'
    this.age =20
}

let p= new Person()

console.log(p)

执行结果:

image.png

请注意,这是在Person()这个函数本身没有返回值时的情况,一旦这个函数有了一个引用类型的数据时,new的执行结果就是Person()返回出来的引用类型的数据

image.png

所以我们可以总结一下new的执行原理:

    1. 创建一个新对象
    1. 往这个新对象里加key
    1. 将函数的this绑定到这个对象中
    1. 此对象的隐式原型等于构造函数中的显示原型
    1. 返回这个对象(判断这个函数的值如果是引用类型,则采用)
function Person(){
    this.name ='小管'
    this.age =20
    return [123]
}
let obj= new Person()
Person.call(obj)
obj.__proto__ = Person.prototype
return Person() instanceof Object ? Person() : obj

其实new中的绑定还是用的是this的绑定

4.箭头函数

先说结论:箭头函数中没有this

function foo(){
    let bar = function() {
      let baz =()=>{
        let fn =()=>{
        console.log(this)
        }
        fn()
      }
      baz()
    }
   bar()
}
foo()

易知,baz() 和 fn() 都是箭头函数,没有this,所以代码中的this是箭头函数外的非箭头函数的。

注意:this是谁的,和指向谁是不同的概念

那如果我们这么写一段代码呢

let Foo =()=>{
    this.name='廖总'
}

let foo = new Foo()

我们想以Foo()为构造函数,new出一个实例对象,当我们运行时我们就会发现

image.png Foo is not a constructor Foo不是一个构造器

由此我们又可以得出一个结论: 箭头函数不能作为构造函数使用

手搓一个call

废话不多说,手搓一个call

function foo(x,y) {
    console.log(this.a, x,y);
    return x+y;
}
let obj = {
    a:1,
}
foo.mycall(obj,1,2)
  • 假设有一个mycall可以实现call的全部功能
function foo(x,y) {
    console.log(this.a, x,y);
    return x+y;
}
Function.prototype.mycall = function (){
    
}

let obj = {
    a:1,
}
foo.mycall(obj,1,2)
    1. 先把mycall挂载在Function的显示原型上,让所有的函数能访问到这个方法。
function foo(x,y) {
    console.log(this.a, x,y);
    return x+y;
}
Function.prototype.mycall = function (...args) {
     const context =  args[0] || window
      const arg = args.splice(1)
}

let obj = {
    a:1,
}
foo.mycall(obj,1,2)
    1. mycall中携带的参数用...args变为一个数组,且将数组中的第一个参数为this上下文
    1. 将剩下的参数,放入arg数组中
function foo(x,y) {
   console.log(this.a, x,y);
   return x+y;
}
Function.prototype.mycall = function (...args) {
    const context =  args[0] || window
    const arg = args.splice(1)
    context.fn=this   
}

let obj = {
   a:1,
}
foo.mycall(obj,1,2)
    1. context上挂载一个fn,因为mycall是因为foo触发的非独立调用,所以这里的this就指向调用mycall的函数foo,也就是说,我们把函数foo挂载到了context身上。
function foo(x,y) {
   console.log(this.a, x,y);
   return x+y;
}
Function.prototype.mycall = function (...args) {
    const context =  args[0] || window
    const arg = args.splice(1)
    context.fn=this 
    const res = context.fn(...arg)
    delete context.fn
    return res
}

let obj = {
   a:1,
}
foo.mycall(obj,1,2)
  • 5. context.fn就代表foo,再解构arg数组,将解构后的参数传入context.fn中,返回的结果保存到res
    1. 删除挂载到context身上的foo
    1. 返回res

大功告成,恭喜你,打造了属于自己的mycall

结语

js中的this指向问题,谈这么多就够了。js之路道阻且长,还得好好努力啊!

6fa6572b19f12fe7f16fac93aff3b72.jpg