likes
comments
collection
share

解刨this指向问题与call,apply,bind,new问题大合集

作者站长头像
站长
· 阅读数 29

最近有在面试中遇到多处被问到指针问题和指针继承的关键字手写,想着将写一篇指针大合集,欢迎大家阅读指点。

由于本文有着原型链和继承的知识,不理解的同学可以先看了解一下:

this指向问题

在深入探讨编程领域的复杂结构与精妙机制时,"this"这一关键字常常成为连接逻辑与语境的微妙纽带。它不仅是JavaScript等面向对象语言中的一个核心概念,更是理解对象行为、方法调用以及函数上下文的关键。本文旨在揭示"this"指向的奥秘,触及背后深层的逻辑原理与实践应用。

this出现的原因

this关键字的出现,本质上是为了在面向对象编程中提供一种机制,使得函数能够访问和操作定义它们的对象的属性和方法,而无需将这些对象作为参数显式传递。这一设计极大地提升了代码的自然表达力、可读性和可维护性,减少了因频繁手动传递上下文而导致的冗余和错误。

在JavaScript中,一个对象的某个方法可以通过this轻松访问该对象的其他属性或方法,这样就避免了外部调用者需要知道或管理对象内部细节的情况,降低了模块间的耦合度。通过利用this,可以更自然地表达对象的行为,促进面向对象编程原则的实现,如封装、继承和多态。

this的指向

this的指向大致可以分为以下几个部分来讨论:

默认绑定

当一个函数独立调用时,不带任何修饰符的调用,该函数的this指向window

在非严格模式下,当一个函数在全局作用域中独立调用时,其this关键字默认指向全局对象。在浏览器环境中,这个全局对象是window;在Node.js环境中,则是globalglobalThis。然而,如果函数在严格模式('use strict';)下调用,this则会被设置为undefined

(以下示例均为非严格模式)

function sayHello() {
    console.log("Hello, " + this);
}

sayHello(); // 控制台输出: "Hello,[object global]"

这里由于sayHello函数直接被调用,没有作为任何对象的方法,因此它的this被默认绑定到了全局对象window上。

隐式绑定

当一个函数被某个对象所拥有,或者函数被某个上下文对象调用时,该函数中this指向该上下文对象

隐式绑定发生在当一个函数作为某个对象的属性被调用时,这时this会自动绑定到该对象。这种绑定方式是JavaScript中最常见的this绑定类型之一。

var person = {
    name: "Alice",
    greet: function() {
        console.log("Hello, my name is " + this.name);
    }
};
person.greet(); // 输出: "Hello, my name is Alice"

这里greet函数是person对象的一个方法。当我们通过person.greet()的形式调用这个函数时,this关键字就会隐式地绑定到person对象上。因此,this.name在函数内部就等同于访问person对象的name属性,输出结果为"Alice"。

间接调用的隐式绑定影响

需要注意的是,如果通过一个变量或表达式来间接调用这样的方法,可能会破坏隐式绑定,导致this不再指向预期的对象。例如,如果将方法赋值给另一个变量再调用,this的绑定就会丢失:

var greetFunc = person.greet;
greetFunc(); // 输出: "Hello, my name is undefined" 或者抛出错误,因为此时this不再绑定到person对象

隐式丢失

当一个函数被多个对象链式调用时,this指向最近的那个对象

在JavaScript中,准确的概念是“this的值根据调用函数的方式来决定,this会绑定到链中的最后一个对象

var obj1 = {
  name:'Alice',
  obj2: {
  name:'Bob',
      greet: function() {
          console.log("Hello from " + this.name);
      }
  }
};
obj1.obj2.greet();// 输出: "Hello from Bob" (表示obj2对象)

显示绑定

通过call ,apply ,bind 将函数的this掰弯到一个指定的对象中

call()

call()方法允许你调用一个函数,并且指定函数内部的this值,同时还可以传递参数列表,注意这个传递列表时是逐个传递

function greet(name, greeting) {
    console.log(greeting + ", " + this.name + " " + name);
}
var person = { name: "Alice" };
greet.call(person, "Bob", "Hello"); // 输出: "Hello, Alice Bob"

手写call()

Function.prototype.myCall = function(context = window, ...args) {
    // 确保调用自一个函数
    if (typeof this !== 'function') {
        throw new TypeError('myCall must be called on a function');
    }
    // 改变上下文并调用函数
    context.fn = this; // 绑定函数到context
    const result = context.fn(...args); // 调用函数并传递参数
    delete context.fn; // 清理,避免污染
    return result;
};

apply()

apply()方法与call()类似,也是用来改变函数调用时的this值,但它接受一个数组或类数组对象作为参数。

function greet(name, greeting) {
    console.log(greeting + ", " + this.name + " " + name);
}
var person = { name: "Alice" };
greet.apply(person, ["Bob", "Hello"]); // 输出: "Hello, Alice Bob"

手写apply()

Function.prototype.myApply = function(context = window, args = []) {
    // 确保调用自一个函数
    if (typeof this !== 'function') {
        throw new TypeError('myApply must be called on a function');
    }
    // 改变上下文并调用函数
    context.fn = this;
    const result = context.fn(...args); 
    delete context.fn; 
    return result;
};

bind()

bind()方法创建一个新的函数,当这个新函数被调用时,this值会被永久绑定到bind()的第一个参数。其余参数可以在调用这个新函数时提供。

function greet(name, greeting) {
    console.log(greeting + ", " + this.name + " " + name);
}
var person = { name: "Alice" };
var greetBob = greet.bind(person, "Bob");
greetBob("Hello"); // 输出: "Hello, Alice Bob"

手写bind()

Function.prototype.myBind = function(context = window, ...boundArgs) {
    // 确保调用自一个函数
    if (typeof this !== 'function') {
        throw new TypeError('myBind must be called on a function');
    }
    // 返回一个新的函数,该函数在调用时将this绑定到指定的context
    return (...callArgs) => {
        return this.myApply(context, [...boundArgs, ...callArgs]);
    };
};

new绑定

this指向实例对象

当使用new关键字调用一个函数时,会发生的“new绑定”。这种情况下,this会被绑定到新创建的实例对象上,这是构造函数创建新对象并初始化其属性的一种方式。

function Person(name) {
    this.name = name;
    this.introduce = function() {
        console.log("Hello, my name is " + this.name);
    };
}
// 使用 'new' 关键字创建Person的实例
var alice = new Person("Alice");
// 调用实例上的方法
alice.introduce(); // 输出: "Hello, my name is Alice"

上面这个例子中,Person函数扮演了一个构造函数的角色。当我们使用new Person("Alice")构造时,this指向新创建的实例对象,使得我们能够在构造函数中初始化实例的属性和方法,

手写new实现:

当我们使用new Obj()调用它时实例化一个对象时,以下过程发生了:

  1. 创建新对象:首先,我们使用Object.create(Constructor.prototype)来创建一个新对象,并将其原型设置为构造函数的prototype属性。这意味着新对象将继承构造函数原型上的所有方法和属性。
  2. 绑定作用域:通过Constructor.apply(instance, args),我们将构造函数Constructor内部的this绑定到新创建的实例instance上,并传递构造函数的参数args。这使得构造函数内部可以使用this来设置实例的属性和方法。
  3. 检查返回值:检查构造函数是否返回了一个对象。如果构造函数显式返回了一个对象(并且不是null、原始值如字符串或数字等),那么这个返回的对象将作为最终的实例返回。这是为了允许构造函数有机会返回一个替代的对象作为实例。
  4. 返回实例:如果没有显式返回一个对象,或者返回的是null或一个原始值,那么我们直接返回之前创建的新对象instance。这是因为通常构造函数的目的是初始化新创建的对象,而不是替换它。
function customNew(Constructor, ...args) {
    // 1. 使用Object.create()创建一个新的空对象
    const instance = Object.create(Constructor.prototype);

    // 2. 将构造函数的作用域绑定到新对象上(即设置`this`)
    const result = Constructor.apply(instance, args);

    // 3. 检查构造函数是否返回了一个对象
    //为了处理构造函数可能返回非基本类型值(如对象)的情况。
    if (typeof result === 'object' && result !== null) {
        return result;
    }

    // 4. 如果构造函数没有返回一个对象,则返回新创建的实例
    return instance;
}

function Person(name, age) {
    this.name = name;
    this.introduce = function() {
        console.log(`Hi, I'm ${this.name}.`);
    };
}
//实例化
const alice = customNew(Person, "Alice"
alice.introduce(); // 输出: Hi, I'm Alice.

这里使用的是apply来绑定的,也可以使用call来替换apply,只是参数传递的方式略有不同。call接受的是参数列表,而不是数组。但是不能推荐大家使用bind进行绑定,因为bind会返回一个新的函数,而不是直接调用构造函数。

转载自:https://juejin.cn/post/7389643363161161738
评论
请登录