JavaScript這門語言總是能帶給我驚喜,在敲代碼的時候習以為常的寫法,退一步再看看發現自己其實對很多基操只有表面的使用,而從來沒思考過為何要這樣操作。
今天整理JS代碼的時候突然發出靈魂三連問:
為什么有些時候操作對象,可以直接調用對象上的方法,但有些時候我們使用類似Array.from()
的寫法?
在對象上調用的方法跟在原型上調用的方法區別是什么?這兩者相同么?
為什么JS上可以直接在基礎類型值上調用對象上面才存在的方法?基礎類型值上調用的方法與在對象上調用的方法有區別么?
不同的方法調用方式
瞟了眼我的代碼,立馬就發現了一個調用類上方法的片段:
const obj = { a: 1 };
console.log(Object.hasOwn(obj, 'a')); // true
// 但是如果在對象上調用,則會拋不存在的錯誤
console.log(obj.hasOwn('a')); // TypeError: obj.hasOwn is not a function
在上面的例子里,Object.hasOwn
是一個可以直接調用的方法,但令人困惑的是,當我們嘗試直接在對象實例上調用hasOwn方法時,卻拋出了一個類型錯誤,是不是有點反直覺? 我仔細想了一想突然發現,其實這只是一個基礎JS概念的一個外在表現,只不過我們習慣了作為現象使用它,卻很少會想到它背后的邏輯。
靜態方法與實例方法
其實,我們需要做的只是區分JavaScript靜態方法和實例方法。
靜態方法 是定義在類上的方法,而不是在類的實例上,靜態方法內部訪問不到this
與實例變量。所以我們只能通過類來調用這些方法,而不能通過一個實例來調用
class MyClass {
static staticMethod() {
console.log('這是個靜態方法');
}
}
MyClass.staticMethod(); // 正常執行
const myInstance = new MyClass();
myInstance.staticMethod(); // Error: myInstance.staticMethod is not a function
實例方法 是定義在類的原型上的方法,實例方法內可以訪問對象的屬性,也可以訪問this
,可以直接在實例化對象上調用這些方法
class MyClass {
instanceMethod() {
console.log('這是個實例/對象方法');
}
}
const myInstance = new MyClass();
myInstance.instanceMethod(); // 正常執行
概括來說,上面例子中Object.hasOwn()
是一個需要傳參的、在Object
這個類上的靜態方法,所以才需要在類上直接調用,而不能在實例對象上調用;但在例如arr.sort()
的調用,實際調用的是實例對象上的方法
至于為何會做如此區分,原因是一個簡單的面向對象編程需求:如果一個方法邏輯不涉及對象上的屬性,但又邏輯上屬于這個類,通過接受參數就可以實現功能的,則可以作為一個類的靜態方法存在。但如果它需要直接訪問類上屬性,直接作為實例方法顯然更加妥當。
原型鏈與方法調用
JavaScript中的每個對象都有一個原型(prototype)(除了Object.protoype
也就是所有原型的盡頭),對象的方法實際上是定義在原型鏈上的。雖然我們可能是在對象上調用了一個方法,實際上JavaScript引擎會沿著原型鏈查找該方法并調用。
const arr = [1, 2, 3];
console.log(arr.join('-')); // "1-2-3"
console.log(Array.prototype.join.call(arr, '-')); // "1-2-3"
上面的例子里,join
方法是數組的實例方法。實例方法可以直接在數組的實例上調用,也可以通過Array.prototype.join.call
的方式來調用,這倆本質上是一樣的。唯一區別是Array.prototype.join.call
允許我們在任何類似數組的對象上調用這個方法,哪怕它不是一個真正的數組。
等等?我們可以在不是數組的值上調用join
?是的
const pseudoArray = { 0: 'one', 1: 'two', 2: 'three', length: 3 };
// ❌顯然object上沒有join方法,這樣調用會報錯
pseudoArray.join(','); // Error: pseudoArray.join is not a function
// 成功在object上調用join!!
const result = Array.prototype.join.call(pseudoArray, ',');
console.log(result); // "one,two,three"
所以,在對象上調用實例方法,等同于按照這個對象的原型鏈一層一層向父類上找同名方法來調用。
基礎類型的自動包裝
雖然其他支持面向對象編程范式的語言也有類似行為,也就是對基本類型的自動包裝和自動拆包,但為了百分百掌握JavaScript的行為與他們的異同,還是再來確定一遍吧
每當我們在基本類型值上(例如"hello"
或6
)上調用方法,JavaScript引擎都會先使用基本類型對應的包裝類型對值進行包裝,調用對應的方法,最后將包裝對象丟掉還原基礎類型。這是個引擎內部的隱式操作,所以我們沒有任何的感知。
JavaScript對于以下的基本類型,都有對應的包裝類型。可以通過typeof
操作結果是基本類型名還是object
來確認:
string
- String
number
- Number
boolean
- Boolean
symbol
- Object
bigint
- Object
讓我們列一下他們基本類型對應包裝類型的使用:
// string
const primitiveString = "hello";
const objectString = new String("hello");
console.log(typeof primitiveString); // "string"
console.log(typeof objectString); // "object"
// number
const primitiveNumber = 42;
const objectNumber = new Number(42);
console.log(typeof primitiveNumber); // "number"
console.log(typeof objectNumber); // "object"
// boolean
const primitiveBoolean = true;
const objectBoolean = new Boolean(true);
console.log(typeof primitiveBoolean); // "boolean"
console.log(typeof objectBoolean); // "object"
// symbol
const primitiveSymbol = Symbol("description");
const objectSymbol = Object(primitiveSymbol);
console.log(typeof primitiveSymbol); // "symbol"
console.log(typeof objectSymbol); // "object"
// bigint
const primitiveBigInt = 123n;
const objectBigInt = Object(primitiveBigInt);
console.log(typeof primitiveBigInt); // "bigint"
console.log(typeof objectBigInt); // "object"
所以,在基本類型上調用方法,等同于創建這個基本類型對應的包裝類型的對象并調用方法,最后拆包并返回原始類型的值。本質上還是調用了同類型包裝行為創建的對象上的方法。
"str".toUpperCase();
// 等同于
(new String("str")).toUpperCase()
// 當然,這里巧了,toUpperCase()本來也沒想返回包裝類型的對象
轉自https://www.cnblogs.com/camwang/p/18259567 作者CamWang
該文章在 2024/7/24 16:28:51 編輯過