深刻理解JS中this关键字

为何要使用this关键字

   看个例子bash

function indetify() {
        retun this.name.toUpperCase()
    }
    
    var obj = {
        name: 'zz'
    }
    indetify.call(obj)  // 'ZZ'
复制代码

这里函数identify中的this指向变量obj,若是不使用this的话,想实现这种效果,就须要显示的传入上下文对象,例如app

function indetify(context) {
        retun context.name.toUpperCase()
    }
    var obj = {
        name: 'zz'
    }
    indetify(obj)  // 'ZZ'
复制代码

当模式愈来愈复杂的时候,显示的传递上下文对象就会变得不太好管理,而this提供了一种更优雅的方式,隐式的"传递"一个对象的引用,所以能够将API设计的更加简洁而且易于复用。ide

对于this的误解

  1. this指向自身
  2. this指向函数做用域(须要明确的是,this在任何状况下都不指向函数的词法做用域)

this究竟是什么

this是在运行的时候被绑定的,并非编写的时候,它的上下文取决于函数调用的各类条件。当一个函数被调用时,会建立一个活动记录(即执行上下文),这个记录包含函数的调用栈(call Stack),调用方式,传入参数等信息,this就是这个记录的一个属性,在函数执行的过程当中找到。函数

要想找到this的绑定对象,首先得找到函数的调用位置,调用位置就在当前执行函数的前一个调用中。测试

function baz () {
        // 当前调用栈是baz
        // 当前调用位置是全局做用域
        console.log('baz')
        bar()   // bar的调用位置
    }
    
    function bar () {
        // 当前的调用栈是baz -> bar
        // 因此当前的调用位置在baz中
        console.log('bar')
        foo()   // foo的调用位置
    }
    
    function foo () {
        // 当前的调用栈是baz -> bar -> foo
        // 因此当前的调用位置在bar中
        console.log('foo')
    }
    baz()   // baz的调用位置
复制代码

找到调用位置,接下来就要分析this的绑定规则了。ui

this绑定规则

  1. 默认绑定规则
function foo () {
    console.log(this.a)
}

var a = 1

foo()   // 1
复制代码

由于foo()是直接不带任何修饰的函数引用进行调用的,因此只能使用默认绑定。非严格模式下指向window,严格模式下绑定到undefinedthis

  1. 隐私绑定规则

分析隐式绑定时,必须在一个对象内部包含一个指向函数的属性,经过这个属性间接引用函数spa

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

var obj = {
    a: 2,
    foo: foo
}

obj.foo()   // 2
复制代码

首先要声明的是,不管foo函数是先声明仍是直接在obj对象中定义,这个函数严格来讲,都不属于obj对象。prototype

隐式绑定会遇到一个问题,就是会丢失绑定对象,也就是会应用默认绑定。好比设计

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

var obj = {
    a: 2,
    foo: foo
}

var another = obj.foo   // 函数别名

var a = '隐式丢失'

obj.foo()   // 2
another()   // '隐式丢失'
复制代码

这里虽然bar是obj.foo的一个引用,但实际上引用的是foo函数自己,所以bar()是不带任何修饰的函数调用,因此也是默认绑定。

还有一种更微妙的状况,发生在传入回调函数的时候

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

var obj = {
    a: 2,
    foo: foo
}

function doFun (fn) {
    // fn 实际上是引用foo
    // 至关于var fn = foo
    fn()
}

var a = '又隐式丢失'

obj.foo()   // 2
doFun(obj.foo)   // '又隐式丢失'
复制代码

实际上传递参数,实际上就是一种赋值操做,因此结果和上面同样

  1. 显示绑定规则

一般状况,咱们使用js提供的call和apply方法实现显示绑定

这俩个方法的第一个参数是一个对象,是给this准备的,在调用函数是将函数绑定到this,所以称为显示绑定。第二个参数就是一个参数列表或参数对象,就是传递函数的调用的参数。

function foo (name) {
    console.log(this.a+name)
}

var obj = {
    a: 1
}

foo.call(obj,'显示绑定')   // 1'显示绑定'
复制代码

可是,显示绑定仍是会出现绑定丢失的状况,能有办法解决吗?固然有

  • 硬绑定
function foo () {
    console.log(this.a)
}

var obj = {
    a: 1
}

var bar = function () {
    foo.call(obj)
}

var a = '会丢失吗'

bar()   // 1

// 如今不会丢失了

setTimeOut(bar, 1000)   // 1
bar.call(window)        // 1
复制代码

咱们建立了函数bar(),而且在内部手动调用foo.call(obj),强制将foo的this绑定到了obj,不管后面如何调用函数bar,都会手动在obj上调用foo

  • API调用的“上下文”

js语言和宿主环境中许多新的内置函数,都提供了一个可选参数,一般称为“上下文”(context),做用和bind(..)同样,确保回调函数使用指定的this

function foo (el) {
        console.log(el, this.id)
    }
    
    var obj = {
        id: 'awesome'
    }
    // 调用foo的同时把this绑定到obj上
    [1,2,3].forEach(foo, obj)
    // 1'awesome' 2'awesome' 3'awesome'
复制代码
  1. new绑定规则

js中的new的机制和面向类的语言彻底不一样

咱们习惯把new的函数叫作构造函数,实际上,只是使用new操做符时被调用的函数,不会实例化一个类,由于实例化的类与类之间是复制,是两位彻底不要紧的,但js不是。只能说这些函数是被new操做符调用的普通函数而已。

首先,咱们看看new操做符会作些什么

  1. 建立(或者说构造)一个全新的对象

  2. 这个对象会被执行[[prototypt]]链(原型链)

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

  4. 若是函数没有返回对象,那new表达式中的函数会自动返回这个新对象

function foo (a) {
        console.log(a)
    }
    
    var bar = new foo(1)
    bar()   // 1
复制代码

优先级

既然this有这么多种绑定方式,确定会存在绑定的优先级

首先,毫无疑问,默认绑定的优先级是最低的

  1. 那隐式绑定和显示绑定的优先级谁高?
funtion foo (){
        console.log(this.a)
    }
    
    var obj1 = {
        a: 1,
        foo: foo
    }
    
    var obj2 = {
        a: 2,
        foo: foo
    }
    
    obj1.foo()  // 1
    obj2.foo()  // 2
    
    // 显示绑定改变了this的指向
    obj1.foo.call(obj2) // 2
    obj.foo.call(obj1)  // 1
复制代码

很显然,显示绑定的优先级比隐式绑定的高。

  1. 隐式绑定和new绑定谁的优先级高
function foo (something) {
        this.a =  somethig
    }
    
    var obj1 = {
        foo: foo
    }
    
    var obj2 = {}
    
    obj1.foo(1)
    console.log(obj1.a) // 1
    
    obj1.foo.call(obj2, 2)
    console.log(obj2.a)  // 2
    
    var bar = new obj1.foo(3)
    
    console.log(obj2.a) // 2
    
    console.log(bar.a)  // 3
    
复制代码

看来new绑定的优先级是比隐式绑定高的,最后咱们看一下new和显示绑定谁的优先级高,由于new和call/apply没法一块儿使用,因此无法经过new foo.call(obj1)来直接测试,咱们选择用硬绑定来测试。

回忆一下硬绑定是如何工做的,Function.prototype.bind(..)会建立一个新的包装函数,这个函数会忽略它当前的this绑定,并把提供的对象绑定到this上。

  1. new和显示绑定谁的优先级高
function foo (something) {
        this.a =  somethig
    }
    
    var obj1 = {}
    
    var bar = foo.bind(obj1)
    bar(1)
    console.log(bar.a)  // 1
    
    var baz = new bar(2)
    
    console.log(obj1.a) // 1
    console.log(baz.a)  // 2
    
复制代码

bar被硬绑定到obj1上,可是new bar(2),并无将obj1.a修改为2,相反,new修改了硬绑定(到obj1的)调用bar(..)中的this。这样看来new调用的函数,新建立的this替换了硬绑定的参数,因此new的优先级是最高的。

那咱们断定优先级的方法就是从优先级由高往下去断定。

绑定例外

凡事都有例外,不是全部的绑定都遵循这个规则的。

  1. 若是是call(null)或者apply(undefined),这里对应的实际上是默认绑定,由于其实,null和undefined只是基础的数据类型,并非对象。

  2. 软绑定

硬绑定能够强制绑定到指定对象,可是这大大下降了函数的灵活性,以后没法使用隐式或显示绑定修改this的指向,因此咱们来实现一下软绑定

// 实现软绑定
    if (!Function.prototype.softBind) {
        Function.prototype.softBind = function (obj) {
            var fn = this
            // 捕获全部curried参数
            var curried = [].slice.call(arguments, 1)
            var bound = function () {
                return fn.apply(
                    (!this || this === (window || global)) ?
                        obj : this,
                    curried.concat.apply(curried, arguments)
                )
            }
            bound.prototype = Object.create(fn.prototype)
            return bound
        }
    }
    
    // 验证
    function foo () {
        console.log("name: " + this.name)
    }
    
    var obj1 = { name: "obj1"}
    var obj2 = { name: "obj2"}
    var obj3 = { name: "obj3"}
    
    var fooOBJ = foo.softBind(obj)
    fooOBJ()    // "obj"
    
    obj2.foo = foo.softBind(obj)
    obj2.foo()  // "obj2"
    
    fooOBJ.call(obj3)   // "obj3"
    
    setTimeOut(obj2.foo, 10)   // "obj"
复制代码

能够看到,软绑定的foo()能够手动的将this绑定到obj2或者obj3,但若是应用默认绑定则将this绑定到obj1上。

  1. this词法

此前的4条规则使用与大部分函数,但在ES6中的箭头函数却不适用,由于箭头函数不是function关键字定义的,而是使用被称为“胖箭头”的操做符 => 定义的。箭头函数不使用this的四种标准规则,而是根据外层的做用域决定this的。因此叫作词法

function foo () {
        // 返回一个箭头函数
        return (a) => {
            // this继承自foo()
            console.log(this.a)
        }
    }
    
    var obj1 = {
        a: 2
    }
    
    var obj2 = {
        a: 3
    }
    
    var bar = foo.call(obj1)
    bar.call(obj2)  // 2
复制代码

foo内部建立的箭头函数会捕获调用时foo()的this,因为foo()的this是绑定到obj1上的,bar(只是引用箭头函数)的this也会绑定到obj1上,箭头函数的绑定没法修改。相似于

function foo () {
        var self = this
        setTimeout(function () {
            console.log(self.a)
        }, 100)
    }
复制代码

关于this的理解就说这么多,欢迎指正和交流。

相关文章
相关标签/搜索