• JavaScript的this为什么这么设计


    this作为JavaScript语言的核心语法,我们在开发中是经常用到的。例如在对象方法中,通过this访问对象属性;在构造函数中,用this初始化实例对象。那么JavaScript为什么会有this呢?

    JS为什么会有this?

    JavaScript是一门函数优先(first-class function)的语言,即函数对象跟其它对象一样具有属性和方法,可以跟普通变量一样赋值给另一个变量。

    那么,当一个函数被赋值给不同变量,在不同的执行上下文调用时,如何访问到当前执行上下文呢?

    var msg = '全局信息'
    function log(){
        console.log(msg);  // 打印消息
    }
    
    // 在全局上下文调用
    log(); // 打印:'全局信息'
    
    let checkObj={
        msg:'校验信息',
        log:log
    }
    checkObj.log();  // 打印:'校验信息' ???
    
    let computeObj={
        msg:'计算信息',
        log:log
    }
    computeObj.log();  // 打印:'计算信息' ???
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    解读上面代码:

    log()被赋值给不同变量,分别在全局上下文,checkObj对象方法和computeObj对象方法中被调用。这里,我们的预期是:

    • 在全局上下文调用时,应该能访问全局变量msg = ‘全局信息’;

    • 在checkObj.log()调用时,应该能访问到checkObj.msg = ‘校验信息’;

    • 在computeObj.log()调用时,应该能访问到computeObj.msg = ‘计算信息’;

    当然,上面代码的执行结果是不符预期的。因为根据词法作用域,log()中的msg就是全局变量msg = ‘全局信息’,跟log()的当前执行上下文无关。

    所以,需要有一种机制,可以在函数内部获取到当前执行上下文,这就是this。

    上面的代码用this重构如下:

    var msg = '全局信息'
    function log(){
        console.log(this.msg);  // 打印当前上下文的消息
    }
    
    // 在全局上下文调用
    log(); // 打印:'全局信息'
    
    let checkObj={
        msg:'校验信息',
        log:log
    }
    checkObj.log();  // 打印checkObj.msg:'校验信息'
    
    let computeObj={
        msg:'计算信息',
        log:log
    }
    computeObj.log();  // computeObj.msg:'计算信息'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    那么,JavaScript的this是怎么设计的呢?

    自动绑定

    容易想到的方法是:当函数被调用时,JavaScript引擎自动将this值设置为当前执行上下文。这样,在函数内部就能通过this访问到当前执行上下文了。

    根据调用方式,函数的执行上下文可以分为两种。

    对象方法调用

    作为对象方法调用时,函数中的this指向该对象。

    var value="123"
    function func(){
    	console.log(this.value)
    }
    
    let obj={
        value:"abc",
        getVal:func
    }
    obj.getVal(); // "abc"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    如果把上面的func()换成下面这种写法:

    var value="123"
    function func(){
        function foo(){
        	console.log(this.value)
        }
    	foo();
    }
    
    let obj={
        value:"abc",
        getVal:func
    }
    obj.getVal(); // "123"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    虽然func()作为obj对象方法被调用,但内部的foo()是在全局执行环境被调用的,所以foo()中的this指向顶层对象window/global。

    全局调用

    在全局执行环境调用函数时,函数中的this指向顶层对象(window或global);

    var value="123"
    function func(){
    	console.log(this.value);
    }
    func(); // "123
    
    • 1
    • 2
    • 3
    • 4
    • 5

    注意:严格模式下,this为undefined。

    var value="123"
    function func(){
        "use strict";
    	console.log(this.value);
    }
    func(); // TypeError: Cannot read properties of undefined (reading 'value')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    手动绑定

    在不同的执行上下文,this会自动绑定不同的值,但有时候我们想手动指定this值,这种情况又得怎么实现呢?

    在es5中,可以通过call(),apply(),bind()手动绑定函数中的this值:

    • call()和apply()的区别就是传参不同,前者是列表参数,后者是数组参数。

    • call()和apply()是在每次函数调用时设置this指向;
      bind()的入参是一个函数,将函数的this永久绑定一个值,然后返回新的函数。

    应用场景:

    call()和apply()常用于高阶函数中,将包裹函数的this传递到内部的函数。例如:限流函数和防抖函数。

    • 限流函数

    当连续调用函数时,限制函数单位时间内只执行一次。适用于限制缩放/滚动/鼠标移动等事件处理函数的执行频率。

    function throttle(fn, interval){
        if(typeof interval !== "number" && !(interval instanceof Number)){
            throw new TypeError("第二个参数是限流时间间隔(ms),请输入正整数")
        }
        let lastExec=0;
        function wrapper(){
            let self=this;
            let args=arguments;
            let pastTime=Date.now()-lastExec;
            function exec(){
                lastExec=Date.now();
                fn.apply(self,args);
            }
            if(pastTime>interval){
                exec()
            }
        }
        return wrapper;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 防抖函数

    当连续调用函数时,保证函数只被执行一次。可以是在连续调用的第一次执行,也可以在最后一次执行。适用于按钮点击防抖,搜索框提交内容。

    function debounce(fn, delay = 200, atBegin=true) {
        let timeoutId = null;
        return function wrapper() {
            let self=this;
            let args=arguments;
            function exec(){
                fn.apply(self,args);
            }
            function clear(){
                timeoutId=undefined;
            }
            if(timeoutId){
                clearTimeout(timeoutId);
            }
            if(atBegin&&!timeoutId){
                exec();
            }
            timeoutId=setTimeout(atBegin?clear:exec,delay)
        }
    }
    
    obj = {
        prop:'1',
        func:debounce(function(){
            console.log(this.prop);
        },3000)
    }
    obj.func(); // 3000ms后打印: 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    解读上面代码

    • debounce()返回的wrapper()是作为obj对象方法func()被调用的,所以wrapper()中的this指向obj。
    • setTimeout()的回调是一个箭头函数,this是属于外层词法作用域的(见下文),即指向obj。
    • fn被调用时,this显式绑定为obj,打印的是obj.prop。

    bind()可以永久绑定函数中的this值。例如绑定回调函数的this。

    // 实现一个Promise类
    class MyPromise {
        // 构造方法
        constructor(executor) {
            // 初始化值
            this.initValue()
            // 初始化this指向
            this.initBind()
            // 执行传进来的函数
            executor(this.resolve, this.reject)
        }
    
        initBind() {
            // 初始化this
            this.resolve = this.resolve.bind(this)
            this.reject = this.reject.bind(this)
        }
    
        initValue() {
            // 初始化值
            this.PromiseResult = null // 终值
            this.PromiseState = 'pending' // 状态
        }
    
        resolve(value) {
            // 如果执行resolve,状态变为fulfilled
            this.PromiseState = 'fulfilled'
            // 终值为传进来的值
            this.PromiseResult = value
        }
    
        reject(reason) {
            // 如果执行reject,状态变为rejected
            this.PromiseState = 'rejected'
            // 终值为传进来的reason
            this.PromiseResult = reason
        }
    }
    
    // 使用MyPromise处理异步操作
    let pro = new MyPromise((resolve,reject)=>{
        setTimeout(()=>{
        resolve('success');
        },2000);
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    解读上面代码

    • 我们把异步操作的完成状态和触发异步回调封装成MyPromise类。
    • 创建MyPromise对象时传入一个函数参数,在执行异步操作后调用resolve(reject)改变状态。
    • resolve(reject)是作为参数传给外部函数调用的,所以为了保证resolve(reject)能正确改变状态,必须将resolve(reject)的this硬绑定到当前的MyPromise对象。
    // bind()的一种实现。
    function bind(fn, obj) {
        return function() {
            return fn.apply( obj, arguments );
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    箭头函数

    箭头函数本身没有this,而是属于外层词法作用域的。

    var value="123"
    function func(){
    	return ()=>this.value
    }
    let obj={
        value:"kobe",
        getValue:func
    }
    
    obj.getValue()(); // "kobe"
    func()() // "123"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    解读上面代码

    • func()返回的箭头函数,其中的this实际就是func()中的this。
    • 当func()作为obj对象方法调用时,this指向obj。
    • 当func()在全局执行环境调用时,this指向顶层对象(window/global)。

    箭头函数的词法作用域特性和this机制的动态特性是JavaScript中两种不同的编码风格。在箭头函数出现之前,我们常用self方法(别名方法)来捕获this:

    function foo() {
        var self = this; // lexical capture of this
        setTimeout( function(){
        	console.log( self.a );
        }, 100 );
    }
    var obj = {
    	a: 2
    };
    foo.call( obj ); // 2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    构造函数

    构造函数中的this指向正在创建的对象。

    • 普通构造函数
    function Factory(){
    	this.name="kobe"
    }
    
    let obj=new Factory();
    console.log(obj.name)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 类构造函数
    class Factory {
        constructor(){
        	this.name="name"
    	}
    }
    let obj1=new Factory();
    console.log(obj1.name)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    总结

    一开始,JS函数内部的this是自动绑定到当前执行上下文的。在全局执行环境调用函数时,this指向顶层对象(window/global);作为对象方法被调用时,this指向调用对象。

    后来,es5引入了call()、apply()、bind()等方法来支持手动绑定this值:

    • apply()、call()仅对当前调用生效。
    • bind()可以永久绑定this值。

    最后,es6引入了箭头函数,它本身没有this,而是属于外层词法作用域的。

    参考资料

    MDN

  • 相关阅读:
    [RK3568 Android11]libusb流程简介
    Go简单的入门:编译和安装应用
    【计算机网络】HTTPS协议的加密流程
    Jenkins Pipeline 方法(函数)定义及调用
    AIGC底层技术介绍
    react native使用TS实现路由
    数据分析:RT-qPCR分析及R语言绘图
    手把手带你刷好题(牛客刷题④)
    gpg: no valid OpenPGP data found.
    开源版小剧场短剧影视小程序源码+支付收益等模式+付费短剧小程序源码+完整源码包和搭建教程
  • 原文地址:https://blog.csdn.net/weixin_39805244/article/details/126690661