深入理解JS中this关键字

栏目: JavaScript · 发布时间: 4年前

内容简介:看个例子这里函数identify中的this指向变量me,如果不使用this的话,想实现这种效果,就需要显示的传入上下文对象,例如当模式越来越复杂的时候,显示的传递上下文对象就会变得不太好管理,而this提供了一种更优雅的方式,隐式的"传递"一个对象的引用,因此可以将API设计的更加简洁并且易于复用。

看个例子

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

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

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

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

对于this的误解

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

this到底是什么

his是在运行的时候被绑定的,并不是编写的时候,它的上下文取决于函数调用的各种条件。当一个函数被调用时,会创建一个活动记录(即执行上下文),这个记录包含函数的调用栈(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的理解就说这么多,欢迎指正和交流。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

离散数学及其应用(英文版·第5版)

离散数学及其应用(英文版·第5版)

Kenneth H.Rosen / 机械工业出版社 / 2003 / 79.00元

本书第4版是全球500多所大学的指之一教材,获得了极大的成功。中文版也已被国内大学广泛有用为教材。第5版在前四版的基础上做了大量的改进,使其成为更有效的教学工具。   本书可作为1至2个学期的离散数学课入门教材,适用于数学、计算机科学、工程等专业的学生。一起来看看 《离散数学及其应用(英文版·第5版)》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具