1.this

在 JavaScript 中,数组是对象,所有对象都从它们的原型继承属性和方法。原型是一种用作创建其他对象基础的“模板对象”。

内置的 push() 方法,它可以将新的项添加到数组的末尾并返回新的长度。这个方法是 Array 原型的一部分,对 JavaScript 中的所有数组都可用.

1
2
console.log(Array.prototype.hasOwnProperty('push')); // 这将返回 true,因为数组有 push 方法
arr.push(4); // 现在 arr 是 [1, 2, 3, 4]

现在,如果你想向所有数组添加一个新的方法,例如 last(),你可以将它添加到 Array 原型中:

1
2
3
Array.prototype.last = function() {
// 这里放置 last 方法的实现
};

扩展内置原型,如 Array 的原型,可能会有潜在风险,因为如果你的方法名称与未来的 JavaScript 更新或其他库的方法名称冲突,可能会导致意想不到的行为。例如,考虑尝试覆盖 Array 原型上的 push() 方法:

1
2
3
4
5
6
Array.prototype.push = function() {
console.log('push 方法已被覆盖!');
};

let nums = [1, 2, 3];
nums.push(4); // push 方法已被覆盖!

如果需要一个内置方法的修改版本,通常建议创建一个单独的方法或函数。例如,你可以开发一个新的函数,将元素附加到数组中,然后记录一条消息:

1
2
3
4
5
6
7
8
function pushAndLog(array, element) {
array.push(element);
console.log('元素 ' + element + ' 已添加到数组中。');
}

let nums = [1, 2, 3];
pushAndLog(nums, 4); // 元素 4 已添加到数组中。
console.log(nums); // [1, 2, 3, 4]

概述
这个问题引导我们进入 JavaScript 编程的一个有趣部分:向内置原型添加新功能。尽管这因为可能会有潜在风险,通常不是推荐做法,但它确实提供了对 JavaScript 灵活和动态特性的深刻理解。在这个挑战中,我们需要向 Array 原型添加一个 last() 方法。这个新方法将返回应用到它的任何数组的最后一个元素,如果数组为空则返回 -1。

在 JavaScript 中,数组是对象,所有对象都从它们的原型继承属性和方法。原型是一种用作创建其他对象基础的“模板对象”。在这个上下文中,JavaScript 的 Array 对象是一个全局对象,包含用于操作数组的方法,这个对象可以通过自定义方法或属性来扩展。

例如,让我们看一下内置的 push() 方法,它可以将新的项添加到数组的末尾并返回新的长度。这个方法是 Array 原型的一部分,对 JavaScript 中的所有数组都可用:

1
2
3
4
5
let arr = [1, 2, 3];

console.log(Array.prototype.hasOwnProperty('push')); // 这将返回 true,因为数组有 push 方法

arr.push(4); // 现在 arr 是 [1, 2, 3, 4]

现在,如果你想向所有数组添加一个新的方法,例如 last(),你可以将它添加到 Array 原型中:

1
2
3
Array.prototype.last = function() {
// 这里放置 last 方法的实现
};

你创建的所有数组现在都可以访问这个 last() 方法:

1
2
let arr = [1, 2, 3];
console.log(arr.last()); // 你的实现将决定这将输出什么

扩展内置原型,如 Array 的原型,可能会有潜在风险,因为如果你的方法名称与未来的 JavaScript 更新或其他库的方法名称冲突,可能会导致意想不到的行为。例如,考虑尝试覆盖 Array 原型上的 push() 方法:

1
2
3
4
5
6
Array.prototype.push = function() {
console.log('push 方法已被覆盖!');
};

let nums = [1, 2, 3];
nums.push(4); // push 方法已被覆盖!

在这种情况下,push() 方法不再将元素附加到数组的末尾。相反,它仅仅在控制台上记录一条消息。

通常不鼓励覆盖内置方法,push() 方法广泛用于 JavaScript,改变其功能可能导致大量的错误和问题。这在处理第三方库或其他开发者的代码时尤其麻烦,因为他们期望 push() 方法按预期工作。

如果需要一个内置方法的修改版本,通常建议创建一个单独的方法或函数。例如,你可以开发一个新的函数,将元素附加到数组中,然后记录一条消息:

1
2
3
4
5
6
7
8
function pushAndLog(array, element) {
array.push(element);
console.log('元素 ' + element + ' 已添加到数组中。');
}

let nums = [1, 2, 3];
pushAndLog(nums, 4); // 元素 4 已添加到数组中。
console.log(nums); // [1, 2, 3, 4]

在这个问题中,你的任务是扩展 Array 原型,包含一个 last() 方法,如果存在,它应该返回数组的最后一个元素,如果数组为空,则返回 -1。

理解这个任务涉及到理解 JavaScript 中的 this 关键字。在这里,JavaScript 中的 this 关键字的行为与其他编程语言略有不同。this 的值取决于函数调用时的上下文。在这个问题中,this 将引用当前调用 last() 方法的对象,它将是一个数组。

在 JavaScript 中,this 的行为与其他编程语言稍有不同。它的值由它的使用上下文决定,这对初学者来说可能会让人感到困惑。因此,了解上下文和 this 在不同情况下所指的对象是至关重要的。

全局上下文

在全局执行上下文中(即,在任何函数之外),this 无论在严格模式还是非严格模式下,都引用全局对象。

在 web 浏览器中,全局对象是 window,所以 this 将引用 window 对象:

1
console.log(this); // 在浏览器上下文中会记录 "[object Window]"

在 Node.js 环境中,全局对象不是 window 而是 global。因此,如果在 Node.js 上下文中运行相同的代码,this 将引用全局对象:

1
console.log(this); // 在 Node.js 上下文中会记录 "[object global]"

函数上下文

在普通函数内部,this 的值取决于函数的调用方式。如果函数在全局上下文中调用,this 在严格模式下将为 undefined,在非严格模式下将引用全局对象。

1
2
3
4
5
function func() {
console.log(this);
}

func(); // 在非严格模式的浏览器上下文中记录 "[object Window]",在严格模式下会记录 "undefined"

但是,当函数充当对象的方法时,this 将引用调用该方法的对象。这展示了 this 的值不绑定于函数本身,而是由函数被调用的方式和位置决定,这个概念称为执行上下文:

1
2
3
4
5
6
7
8
9
10
let obj = {
prop: "Hello",
func: function

() {
console.log(this.prop);
}
}

obj.func(); // 记录 "Hello"

然而,箭头函数不具有自己的 this。相反,它们从创建时的父作用域继承 this。换句话说,箭头函数内部的 this 值不由它的调用方式决定,而是由它的定义时的外部词法上下文决定:

1
2
3
4
5
6
7
8
let obj = {
prop: "Hello",
func: () => {
console.log(this.prop);
}
}

obj.func(); // 记录 "undefined",因为箭头函数内部的 `this` 不绑定到 `obj`,而是绑定到其外部词法上下文

这在某些情况下可能很有用,但它也使得箭头函数不适合需要访问它们被调用的对象的其他属性的方法。

事件处理程序

在事件处理程序的上下文中,this 引用附加了事件监听器的元素,与 event.currentTarget 相同。

1
2
3
button.addEventListener('click', function() {
console.log(this); // 记录按钮的整个 HTML 内容
});

重要的是注意,它不引用常用的 event.target 属性。让我们澄清 event.currentTarget 和 event.target 之间的区别。

event.currentTarget:该属性引用附加了事件处理程序(如 addEventListener)的元素。这是在事件处理程序函数的上下文中 this 引用的内容。

event.target:该属性引用引发事件的实际 DOM 元素。对于会冒泡的事件特别重要。如果你点击内部元素,事件将冒泡到外部元素,触发它们的事件监听器。对于这些外部元素,event.target 将是实际被点击的最内层元素,而 event.currentTarget(或 this)将是当前处理程序附加到的元素。

点击我
或者点击我
1
2
3
4
5
6
7
<script>
document.getElementById('outer').addEventListener('click', function(event) {
console.log("currentTarget: ", event.currentTarget.id);
console.log("this: ", this.id);
console.log("target: ", event.target.id);
});
</script>

在这种情况下,如果你点击外部 div,所有三个日志都将打印 “outer”,因为点击的元素(target)和处理程序附加的元素(currentTarget 或 this)是相同的。但是,如果你点击内部 div 中的 “或者点击我” 文本,event.target 将是 “inner”(因为这是你点击的元素),而 event.currentTarget(或 this)仍将是 “outer”(因为这是事件处理程序附加的元素)。

构造函数上下文

在构造函数内部,this 引用新创建的对象。但是,这里的“新创建”是什么意思呢?要理解这一点,我们需要探讨 JavaScript 中的 new 关键字。当你在函数调用之前使用 new 时,它告诉 JavaScript 进行四个操作:

创建一个新的空对象。这不是一个函数、数组或 null,只是一个空对象。
使函数内部的 this 引用这个新对象。新对象与构造函数内的 this 关联起来。这就是为什么 Person(name) 内的 this.name 实际上修改了新对象。
正常执行函数。它像通常情况下执行函数代码一样执行。
如果函数没有返回自己的对象,则返回新对象。如果构造函数返回一个对象,那个对象将被返回,而不是新对象。如果返回其他任何内容,将返回新对象。
new 关键字允许 JavaScript 开发者以面向对象的方式使用语言,从构造函数中创建实例,就像其他语言中的类一样。这也意味着构造函数内部的 this 关键字将像从基于类的语言中转换的开发者所期望的那样引用对象的新实例。

1
2
3
4
5
6
7
8
function Person(name) {
// 当使用 `new` 调用时,这是一个新的、空的对象
this.name = name; // `this` 现在有一个 `name` 属性
// 函数结束后,将返回 `this`,因为没有其他对象被函数返回
}

let john = new Person('John'); // `john` 现在是函数 `Person` 返回的对象,包含一个值为 'John' 的 `name` 属性
console.log(john.name); // 记录 "John"

类上下文

在类中,方法内部的 this 引用类的实例:

1
2
3
4
5
6
7
8
9
10
11
12
class ExampleClass {
constructor(value) {
this.value = value;
}

logValue() {
console.log(this.value);
}
}

const exampleInstance = new ExampleClass('Hello');
exampleInstance.logValue(); // 记录 "Hello"

显式 / 隐式绑定

你还可以使用函数上的 .call()、.apply() 或 .bind() 方法来明确设置 this 的上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function logThis() {
console

.log(this);
}

const obj1 = { number: 1 };
const obj2 = { number: 2 };

logThis.call(obj1); // 记录 obj1
logThis.call(obj2); // 记录 obj2

const boundLogThis = logThis.bind(obj1);
boundLogThis(); // 记录 obj1
绑定方法和永久 this 上下文

JavaScript 提供了一个名为 bind 的内置方法,允许我们设置方法中的 this 值。这个方法创建一个新函数,当调用时,将其 this 关键字设置为提供的值,以及在调用新函数时提供的一系列参数。

bind 方法的独特之处在于它创建了一个永久绑定的 this 值,无论后来如何调用该函数,都不会更改 this 的值。下面的示例演示了 bind 如何提供一种锁定函数中的 this 值的方法,在各种情况下都很有帮助,例如在设置事件处理程序时,希望 this 值始终引用特定对象,或者在使用调用回调函数的库或框架时,希望在回调中控制 this 引用的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
JavaScript
function greet() {
return `你好,我是 ${this.name}`;
}

let person1 = { name: 'Alice' };
let person2 = { name: 'Bob' };

// 创建一个与 `person1` 绑定的函数
let greetPerson1 = greet.bind(person1);

console.log(greetPerson1()); // 你好,我是 Alice

// 尝试使用 `call` 方法更改上下文;但是,它仍然使用 `person1` 作为 `this` 上下文
console.log(greetPerson1.call(person2)); // 你好,我是 Alice

// 相比之下,正常函数调用允许使用 `call` 方法设置 `this` 上下文
console.log(greet.call(person2)); // 你好,我是 Bob

在 JavaScript 中,了解 this 关键字的上下文对于操作和与对象交互非常重要,特别是在处理面向对象编程、事件处理程序和函数调用的某些方面。了解 this 的行为有助于改善代码的结构,并使其更可预测和更容易调试。此外,某些设计模式,如工厂模式和装饰器模式,大量使用 this,因此了解其行为对于有效实现这些模式至关重要。

JavaScript 中的一个关键概念是函数对象中的 this 值通常不是固定的 - 它通常是根据函数的执行上下文而确定的,而不是根据其定义的时刻。然而,也有例外情况。使用函数上的 bind()、call() 或 apply() 方法时,这些方法允许你显式设置函数调用的 this 值,从而覆盖其默认行为。此外,JavaScript 中的箭头函数行为不同。它们不绑定自己的 this 值。相反,它们从定义它们的外部词法环境中捕获 this 的值,并且这个值在函数的整个生命周期内保持不变。这些差异使得理解和使用 JavaScript 中的 this 既具有挑战性又非常重要。