• js中this指向的问题与联系深入探究


    前言

    JavaScript 中最大的一个安全问题,也是最令人困惑的一个问题,就是在某些情况下this的值是如何确定的。有js基础的同学面对这个问题基本可以想到:this的指向和函数调用的方式相关。这当然是正确的,然而,这几种方式有什么联系吗?这是我接下来要说明的问题。

    this从哪里来

    this 是js的一个关键字,和arguments类似,它是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用。这句话似乎与认知不同,我们在函数体外部即全局作用域下也能使用this

    // 直接在全局作用域下输出this
    console.log(this);
    // 输出window
    

    但是不要忘记,即便是全局作用域,依旧是运行在window下的,我们写的代码都在window的某个函数中。而这也催生了一种理解this指向的方法:this永远指向调用者(非箭头函数中)。

    作为普通函数调用

    函数作为普通函数直接调用(也称为自执行函数)的时候,无论函数在全局还是在另一个函数中,this都是指向window

    function fn() {
        this.author = 'Wango';
    }
    
    fn();
    console.log(author);
    // Wango
    

    这很好理解,但又不是很好理解,因为在代码中省略了window,补全后就好理解了:this指向的是调用者。

    function fn() {
        this.author = 'Wango';
    }
    
    window.fn();
    console.log(window.author);
    // Wango
    

    而在内部函数中,自执行函数中的this依旧指向全局作用域,我们无法通过window.foo()调用函数,但并不妨碍我们先这样理解(具体参见本文最后一部分this的强制转型)。

    function fn() {
        function foo() {
            console.log(this);
        }
        foo();
        // Window
        window.foo();
        // TypeError
    }
    
    fn();
    

    作为构造函数调用

    在构造函数中,this指向new生成的新对象,即构造函数是通过new调用的,构造函数内部的this当然就应该指向new出来的对象。

    function Person(name, age) {
        this.name = name;
        this.age = age;
        console.log(this);
        // Person { name: 'Wango', age: 24 }
    }
    
    new Person('Wango', 24);
    

    构造函数中的this与构造函数的返回值类型无关,下列代码中p指向了构造函数返回的对象,而不是new出来的对象。当然,这是构造函数的特性,与本主题关系不大。

    function Person(name, age) {
        console.log(this);
        // Person {}
        this.name = name;
        this.age = age;
        console.log(this);
        // Person { name: 'Wango', age: 24 }
    
        return {
            name: 'Lily',
            age: 25
        }
    }
    
    Person.prototype.sayName = function() {
        return this.name + ' ' + this.age
    }
    
    const p = new Person('Wango', 24);
    console.log(p.sayName());
    // TypeError: p.sayName is not a function
    

    作为对象方法调用

    通过对象方法调用时,this指向应该是最明晰的了。与其他面向对象语言的this行为相同,指向该方法的调用者。

    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    
    Person.prototype.sayName = fn;
    
    function fn() {
        return this.name + ' ' + this.age
    }
    
    const p = new Person('Wango', 24);
    console.log(p);
    // Person { name: 'Wango', age: 24 }
    console.log(p.sayName());
    // Wango 24
    

    通过[]调用对象方法

    通常,我们对于对象方法是通过.语法调用,但通过[]也可以调用对象方法,在这种情况下的this指向常常会被我们混淆、忽略。

    function fn() {
        console.log(this);
    }
    
    const arr = [fn, 1];
    
    arr[0]();
    // [Function: fn, 1]
    
    function fn2() {
        arguments[0]();
    }
    
    fn2(fn, 1);
    // [Arguments] { '0': [Function: fn], '1': 1 }
    

    在上例中,无论是数组还是伪数组,其本质上都是对象,在通过[]获取函数元素并调用的时候,会改变函数中的this指向,this指向这个数组或伪数组,与对象调用函数的行为一致。

    通过call、apply调用

    function fn() {
        console.log(this.name);
    }
    
    const author = {
        name: 'Wango'
    }
    
    fn.call(author);
    // Wango
    

    这似乎与this永远指向调用者相违背,但一旦我们明白了call函数的实现机制就会明白,这不仅不是违背,反而是佐证。对callapplybind实现机制不熟悉的同学可以参考我另一篇文章,下面截取call简要说明。

    // 保存一个全局变量作为默认值
    const root = this;
    
    Function.prototype.myCall = function(context, ...args) {
        if (typeof context === 'object') {
            // 如果参数是null,使用全局变量
            context = context || root;
        } else {
            // 参数不是对象的创建一个空对象
            context = Object.create(null);
        }
        // 使用Symbol创建唯一值作为函数名
        let fn = Symbol();
        context[fn] = this;
        context[fn](...args);
        delete context[fn];
    }
    

    call 函数最核心的实现在于context[fn] = this;context[fn](...args);这两行。实际上就是将没有函数调用者的普通函数挂载到指定的对象上,这时this指向与对象调用方法的一致。而delete context[fn];是在调用后立即解除对象与函数之间的关联。

    严格模式下的不同表现

    this强制转型

    使用函数的apply()call()方法时,在非严格模式下nullundefined值会被强制转型为全局对象。在严格模式下,则始终以指定值作为函数this的值,无论指定的是什么值。这也是为何在严格模式下,自执行函数的this不再指向window,而是指向undefined的根本原因。

    // 定义一个全局变量
    color = "red";
    function displayColor() {
        console.log(this.color);
    }
    // 在非严格模式下使用call修改this指向,并指定null,或undefined,
    displayColor.call(null);
    displayColor.call();
    // red
    // 修改指向无效,传入null或undefined被转换为了window
    

    实际上,我们也可以将自执行函数,如fn(),看作是fn.call()的语法糖,在普通模式下,第一个参数默认为undefined,但被强制转换为window。这也就解释了为何所有自执行函数中this都指向window但无法通过window调用的问题(函数在call函数中挂载到window对象上,执行后被立即删除,所以无法再次通过window访问)。

    apply()call()方法在严格模式下传入简单数据类型作为第一个参数时,该简单数据类型会被转换为相应的包装类,而非严格模式不会如此转换。

    function foo() {
        console.log(this);
    }
    
    foo.call(); // Window {}
    foo.call(2); // Number {2}
    
    
    function foo() {
        console.log(this);
    }
    
    foo.call(); // undefined
    foo.call(2); // 2
    

    箭头函数的this指向

    在箭头函数中, this引用的是定义箭头函数的上下文。即箭头函数中的this不会随着函数调用方式的改变而改变。

    function Person(name) {
        this.name = name;
    
        this.getName = () => console.log(this.name);
    }
    
    const p = new Person('Wango');
    
    p.getName();
    // Wango
    
    const getName = p.getName;
    
    getName();
    // Wango
    getName.call({name: 'Lily'});
    // Wango
    

    参考资料:

    Javascript 的 this 用法

    Javascript高级程序设计(第四版)

  • 相关阅读:
    POJ 1751 Highways (kruskal)
    POJ 2031 Building a Space Station
    UVA 624
    POJ 1502 MPI Maelstrom (Dijkstra)
    POJ 3259 Wormholes(SPFA判负环)
    HZAU 1199 Little Red Riding Hood(水DP)
    HZAU 1205 Sequence Number(最大值前后缀 +双指针 + 二分)
    HZAU 1209 Deadline (hash 贪心 水题不水)
    STL完整版整理
    set集合完整版整理
  • 原文地址:https://www.cnblogs.com/hycstar/p/14437820.html
Copyright © 2020-2023  润新知