深入理解JS中this关键字

为什么要使用this关键字

   看个例子

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

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

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

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

对于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的绑定规则了。

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

var a = 1

foo()   // 1
复制代码

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

  1. 隐私绑定规则

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

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

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

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

首先要声明的是,无论foo函数是先声明还是直接在obj对象中定义,这个函数严格来说,都不属于obj对象。

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

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的理解就说这么多,欢迎指正和交流。

转载于:https://juejin.im/post/5cf4e610f265da1b897aba93

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值