解刨this指向问题与call,apply,bind,new问题大合集
最近有在面试中遇到多处被问到指针问题和指针继承的关键字手写,想着将写一篇指针大合集,欢迎大家阅读指点。
由于本文有着原型链和继承的知识,不理解的同学可以先看了解一下:
this指向问题
在深入探讨编程领域的复杂结构与精妙机制时,"this"这一关键字常常成为连接逻辑与语境的微妙纽带。它不仅是JavaScript等面向对象语言中的一个核心概念,更是理解对象行为、方法调用以及函数上下文的关键。本文旨在揭示"this"指向的奥秘,触及背后深层的逻辑原理与实践应用。
this出现的原因
this
关键字的出现,本质上是为了在面向对象编程中提供一种机制,使得函数能够访问和操作定义它们的对象的属性和方法,而无需将这些对象作为参数显式传递。这一设计极大地提升了代码的自然表达力、可读性和可维护性,减少了因频繁手动传递上下文而导致的冗余和错误。
在JavaScript中,一个对象的某个方法可以通过this
轻松访问该对象的其他属性或方法,这样就避免了外部调用者需要知道或管理对象内部细节的情况,降低了模块间的耦合度。通过利用this
,可以更自然地表达对象的行为,促进面向对象编程原则的实现,如封装、继承和多态。
this的指向
this
的指向大致可以分为以下几个部分来讨论:
默认绑定
当一个函数独立调用时,不带任何修饰符的调用,该函数的this指向window
在非严格模式下,当一个函数在全局作用域中独立调用时,其this
关键字默认指向全局对象。在浏览器环境中,这个全局对象是window
;在Node.js环境中,则是global
或globalThis
。然而,如果函数在严格模式('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()
调用它时实例化一个对象时,以下过程发生了:
- 创建新对象:首先,我们使用
Object.create(Constructor.prototype)
来创建一个新对象,并将其原型设置为构造函数的prototype
属性。这意味着新对象将继承构造函数原型上的所有方法和属性。 - 绑定作用域:通过
Constructor.apply(instance, args)
,我们将构造函数Constructor
内部的this
绑定到新创建的实例instance
上,并传递构造函数的参数args
。这使得构造函数内部可以使用this
来设置实例的属性和方法。 - 检查返回值:检查构造函数是否返回了一个对象。如果构造函数显式返回了一个对象(并且不是
null
、原始值如字符串或数字等),那么这个返回的对象将作为最终的实例返回。这是为了允许构造函数有机会返回一个替代的对象作为实例。 - 返回实例:如果没有显式返回一个对象,或者返回的是
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