【力扣】2619. 數組原型對象的最后一個元素——認識原型與原型鏈
文章目錄
- 【力扣】2619. 數組原型對象的最后一個元素——認識原型與原型鏈
- 題目
- 解決方案
- 概述
- 全局上下文
- 函數上下文
- 事件處理程序
- 構造函數上下文
- 類上下文
- 顯式 / 隱式綁定
- 綁定方法和永久 this 上下文
- 方法 1:擴展數組原型以包含 .last() 方法
- 概述
- 算法步驟
- 實現
- 實現 1:常規 if 檢查
- 實現 2:三元運算符
- 實現 3:Nullish 合并運算符
- 實現 4:使用數組 pop() 方法
- 實現 5:將 Nullish 合并運算符與 Array.prototype.at() 方法結合使用
- 實現 6:使用 Array.prototype.slice() 方法
- 實現 7:使用默認參數
- 實現 8:findLast 方法(適用于 ECMAScript 2022 及之后版本)
- 復雜度分析
- 方法 2:使用 ES6 Getters
- 概述
- 算法
- 實現
- 復雜度分析
題目
請你編寫一段代碼實現一個數組方法,使任何數組都可以調用 array.last()
方法,這個方法將返回數組最后一個元素。如果數組中沒有元素,則返回 -1
。
你可以假設數組是 JSON.parse
的輸出結果。
示例 1 :
輸入:nums = [null, {}, 3]
輸出:3
解釋:調用 nums.last() 后返回最后一個元素: 3。
示例 2 :
輸入:nums = []
輸出:-1
解釋:因為此數組沒有元素,所以應該返回 -1。
提示:
arr
是一個有效的 JSON 數組0 <= arr.length <= 1000
解決方案
概述
這個問題引導我們進入 JavaScript 編程的一個有趣部分:向內置原型添加新功能。盡管這因為可能會有潛在風險,通常不是推薦做法,但它確實提供了對 JavaScript 靈活和動態特性的深刻理解。在這個挑戰中,我們需要向Array
原型添加一個last()
方法。這個新方法將返回應用到它的任何數組的最后一個元素,如果數組為空則返回 -1。
在 JavaScript 中,數組是對象,所有對象都從它們的原型繼承屬性和方法。原型是一種用作創建其他對象基礎的“模板對象”。在這個上下文中,JavaScript 的 Array
對象是一個全局對象,包含用于操作數組的方法,這個對象可以通過自定義方法或屬性來擴展。
例如,讓我們看一下內置的push()
方法,它可以將新的項添加到數組的末尾并返回新的長度。這個方法是Array
原型的一部分,對 JavaScript 中的所有數組都可用:
let arr = [1, 2, 3];console.log(Array.prototype.hasOwnProperty('push')); // 這將返回 true,因為數組有 push 方法arr.push(4); // 現在 arr 是 [1, 2, 3, 4]
現在,如果你想向所有數組添加一個新的方法,例如 last()
,你可以將它添加到 Array
原型中:
Array.prototype.last = function() {// 這里放置 last 方法的實現
};
你創建的所有數組現在都可以訪問這個last()
方法:
let arr = [1, 2, 3];
console.log(arr.last()); // 你的實現將決定這將輸出什么
擴展內置原型,如Array
的原型,可能會有潛在風險,因為如果你的方法名稱與未來的 JavaScript 更新或其他庫的方法名稱沖突,可能會導致意想不到的行為。例如,考慮嘗試覆蓋Array
原型上的push()
方法:
Array.prototype.push = function() {console.log('push 方法已被覆蓋!');
};let nums = [1, 2, 3];
nums.push(4); // push 方法已被覆蓋!
在這種情況下,push()
方法不再將元素附加到數組的末尾。相反,它僅僅在控制臺上記錄一條消息。
通常不鼓勵覆蓋內置方法,push()
方法廣泛用于 JavaScript,改變其功能可能導致大量的錯誤和問題。這在處理第三方庫或其他開發者的代碼時尤其麻煩,因為他們期望push()
方法按預期工作。
如果需要一個內置方法的修改版本,通常建議創建一個單獨的方法或函數。例如,你可以開發一個新的函數,將元素附加到數組中,然后記錄一條消息:
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
對象:
console.log(this); // 在瀏覽器上下文中會記錄 "[object Window]"
在Node.js
環境中,全局對象不是window
而是 global
。因此,如果在Node.js
上下文中運行相同的代碼,this
將引用全局對象:
console.log(this); // 在 Node.js 上下文中會記錄 "[object global]"
函數上下文
在普通函數內部,this
的值取決于函數的調用方式。如果函數在全局上下文中調用,this
在嚴格模式下將為 undefined
,在非嚴格模式下將引用全局對象。
function func() {console.log(this);
}func(); // 在非嚴格模式的瀏覽器上下文中記錄 "[object Window]",在嚴格模式下會記錄 "undefined"
但是,當函數充當對象的方法時,this
將引用調用該方法的對象。這展示了this
的值不綁定于函數本身,而是由函數被調用的方式和位置決定,這個概念稱為執行上下文:
let obj = {prop: "Hello",func: function() {console.log(this.prop);}
}obj.func(); // 記錄 "Hello"
然而,箭頭函數不具有自己的 this
。相反,它們從創建時的父作用域繼承 this
。換句話說,箭頭函數內部的 this
值不由它的調用方式決定,而是由它的定義時的外部詞法上下文決定:
let obj = {prop: "Hello",func: () => {console.log(this.prop);}
}obj.func(); // 記錄 "undefined",因為箭頭函數內部的 `this` 不綁定到 `obj`,而是綁定到其外部詞法上下文
這在某些情況下可能很有用,但它也使得箭頭函數不適合需要訪問它們被調用的對象的其他屬性的方法。
事件處理程序
在事件處理程序的上下文中,this
引用附加了事件監聽器的元素,與event.currentTarget
相同。
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)將是當前處理程序附加到的元素。
<div id="outer">點擊我<div id="inner">或者點擊我</div>
</div><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
關鍵字將像從基于類的語言中轉換的開發者所期望的那樣引用對象的新實例。
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
引用類的實例:
class ExampleClass {constructor(value) {this.value = value;}logValue() {console.log(this.value);}
}const exampleInstance = new ExampleClass('Hello');
exampleInstance.logValue(); // 記錄 "Hello"
顯式 / 隱式綁定
你還可以使用函數上的 .call()
、.apply()
或.bind()
方法來明確設置 this
的上下文:
function logThis() {console.log(this);
}const obj1 = { number: 1 };
const obj2 = { number: 2 };logThis.call(obj1); // 記錄 obj1
logThis.call(obj2); // 記錄 obj2const boundLogThis = logThis.bind(obj1);
boundLogThis(); // 記錄 obj1
綁定方法和永久 this 上下文
JavaScript 提供了一個名為bind
的內置方法,允許我們設置方法中的this
值。這個方法創建一個新函數,當調用時,將其this
關鍵字設置為提供的值,以及在調用新函數時提供的一系列參數。
bind
方法的獨特之處在于它創建了一個永久綁定的this
值,無論后來如何調用該函數,都不會更改this
的值。下面的示例演示了bind
如何提供一種鎖定函數中的this
值的方法,在各種情況下都很有幫助,例如在設置事件處理程序時,希望this
值始終引用特定對象,或者在使用調用回調函數的庫或框架時,希望在回調中控制this
引用的對象。
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
既具有挑戰性又非常重要。
方法 1:擴展數組原型以包含 .last() 方法
概述
根據問題陳述,您需要增強所有數組,使其具有返回數組最后一個元素的方法 .last()
。如果數組中沒有元素,則應返回-1。
為此,您可以向數組原型添加一個新方法。這個新方法可以通過訪問這個this[this.length-1]
簡單地返回數組的最后一個元素。
添加到數組原型的方法中的this
關鍵字引用調用該方法的數組。
注意:擴展原生原型是 JavaScript 的一個強大功能,但應該謹慎使用。如果其他代碼(或更高版本的 JavaScript)添加了同名的方法,則可能會導致沖突。在擴展本機原型時始終保持謹慎。
算法步驟
在名為 last 的數組原型上定義一個新方法。
在這個方法中,檢查數組是否為空。如果是,返回 -1。
如果數組不為空,則返回數組的最后一個元素。最后一個元素可以通過以下方式訪問:this[this.length - 1]。
實現
這種方法可以通過各種方式實現。
實現 1:常規 if 檢查
Array.prototype.last = function() {if (this.length === 0) {return -1;}return this[this.length - 1];
}
實現 2:三元運算符
Array.prototype.last = function() {return this.length === 0 ? -1 : this[this.length - 1];
}
這個版本使用了一個三元運算符,代碼更簡潔。? 和 : 就像一個簡短 if/else。
實現 3:Nullish 合并運算符
Array.prototype.last = function() {return this[this.length - 1] ?? -1;
}
此版本使用空合并運算符(??)。如果不為null
或 undefined
,則返回左側操作數,否則返回右側操作數。
請注意,此實現假定數組只包含數字。如果數組的最后一個元素為空或未定義,則此方法將返回-1,這可能會掩蓋最后一個元素的實際值。它可能不適合包含其他數據類型的數組,在這些數組中,null
或 undefined
是有效且不同的值。始終確保使用適合數組中包含的數據類型的方法。
實現 4:使用數組 pop() 方法
Array.prototype.last = function() {let val = this.pop();return val !== undefined ? val : -1;
}
此版本使用數組pop()
方法,該方法從數組中移除最后一個元素并返回它。如果數組為空,則pop()
返回 undefined
,我們檢查它并將其替換為 -1。需要注意的是,該操作會改變原始數組,這可能并不理想,具體取決于您的用例。
實現 5:將 Nullish 合并運算符與 Array.prototype.at() 方法結合使用
Array.prototype.last = function() {return this.at(-1) ?? -1;
}
在此版本中,我們使用ECMAScript 2021
中引入的 Array.prototype.at()
方法。此方法接受一個整數值,并返回該索引處的元素,允許使用正整數和負整數。負整數從數組末尾開始計數。如果數組為空,則at(-1)
將是未定義的,因此我們提供 -1 作為備用。
實現 6:使用 Array.prototype.slice() 方法
Array.prototype.last = function() {return this.length ? this.slice(-1)[0] : -1;
}
在這種方法中,我們使用Array.prototype.slice()
方法。此方法提取數組的一部分并返回新數組。我們通過提供 -1 作為參數來請求最后一個元素。如果數組為空,則slice(-1)[0]
將為 undefined
,因此我們提供 -1 作為備用。需要注意的是,該方法不會改變原始數組,這與我們前面提到的pop()
方法不同。
實現 7:使用默認參數
Array.prototype.last = function() {const [lastElement = -1] = this.slice(-1);return lastElement;
}
此實施使用帶缺省值的 ES6 解構。它本質上與slice(-1)[0]
版本相同,但具有不同的語法。
實現 8:findLast 方法(適用于 ECMAScript 2022 及之后版本)
此版本使用 Array.prototype.findLast()
,這是為 ECMAScript 2022 建議的一種方法,用于查找數組中滿足所提供測試函數的最后一個元素。在這里,我們提供了一個始終返回 true 的函數,因此它將返回最后一個元素,如果數組為空,則返回 -1。
請注意,此解決方案可能在某些情況下不起作用,因為findLast()
尚未得到廣泛支持。請始終查看當前的 JavaScript 文檔,了解其可用性和兼容性。如果要在不支持findLast()
的環境中使用 findLast()
,可以創建 polyfill
:
if (!Array.prototype.findLast) {Array.prototype.findLast = function(predicate) {for (let i = this.length - 1; i >= 0; i--) {if (predicate(this[i], i, this)) {return this[i];}}return undefined;};
}
以下是完整的解決方案,我們還包括findLast()
的 polyfill
,根據您的環境可能不需要:
if (!Array.prototype.findLast) {Array.prototype.findLast = function(predicate) {for (let i = this.length - 1; i >= 0; i--) {if (predicate(this[i], i, this)) {return this[i];}}return undefined;};
}Array.prototype.last = function() {return this.findLast(() => true) ?? -1;
}
復雜度分析
時間復雜度:O(1)。無論數組的大小如何,我們只訪問數組的最后一個元素,這是一個恒定的時間操作。
空間復雜度:O(1)。這是因為我們沒有使用任何隨輸入數組大小而擴展的額外空間。在空間復雜性分析中不考慮數組本身,因為它是函數的輸入。我們只考慮該函數使用的任何額外空間。
需要注意的是,就時間和空間復雜性而言,將方法添加到陣列原型不會影響其他陣列,因為它不會為每個陣列重復該方法。相反,該方法駐留在原型中,并且可以由所有數組訪問。這使得它成為一種高度節省空間的操作。
方法 2:使用 ES6 Getters
概述
在 JavaScript 中,getter
是獲取特定屬性的值的方法。在這里,我們將為最后一個屬性創建一個 getter
。
算法
- 通過為最后一個屬性定義一個
getter
來增強數組原型。 getter
函數將返回另一個函數,該函數返回數組的最后一個元素,如果數組為空,則返回 -1。
實現
Object.defineProperty(Array.prototype, 'last', {get: function() {return () => this.length ? this[this.length - 1] : -1;}
});
當你定義一個getter
時,你實際上是把 last 當作一個屬性而不是一個函數。因此,它是通過array.last
而不是array.last()
訪問的。如果您將數組的最后一個元素視為該數組的屬性,而不是函數的結果,則這種觀點在語義上會更清晰。Getter
可以提供一種更精煉的、類似于屬性的語法,以增強可讀性,特別是當您要實現的操作不需要任何參數并且在概念上是一個屬性時。
此外,當在大量使用getter
和setter
的代碼庫中工作時,使用getter
可以提高一致性。然而,在您的特定問題的上下文中注意到這一點很重要:因為getter
被視為一個屬性,所以需要一個嵌套的函數來通過在線判斷。這個附加層提供了一種與屬性交互的方法,使評測機能夠實現預期的適當功能。
復雜度分析
時間復雜度:O(1)。在 JavaScript 中,訪問數組中特定索引處的元素是一個恒定的時間操作。
空間復雜度:O(1)。不會使用額外的空間。