JavaScript这门语言总是能带给我惊喜,在敲代码的时候习以为常的写法,退一步再看看发现自己其实对很多基操只有表面的使用,而从来没思考过为何要这样操作。
今天整理JS代码的时候突然发出灵魂三连问:
为什么有些时候操作对象,可以直接调用对象上的方法,但有些时候我们使用类似Array.from()
的写法?
在对象上调用的方法跟在原型上调用的方法区别是什么?这两者相同么?
为什么JS上可以直接在基础类型值上调用对象上面才存在的方法?基础类型值上调用的方法与在对象上调用的方法有区别么?
不同的方法调用方式
瞟了眼我的代码,立马就发现了一个调用类上方法的片段:
const obj = { a: 1 };
console.log(Object.hasOwn(obj, 'a'));
console.log(obj.hasOwn('a'));
在上面的例子里,Object.hasOwn
是一个可以直接调用的方法,但令人困惑的是,当我们尝试直接在对象实例上调用hasOwn方法时,却抛出了一个类型错误,是不是有点反直觉? 我仔细想了一想突然发现,其实这只是一个基础JS概念的一个外在表现,只不过我们习惯了作为现象使用它,却很少会想到它背后的逻辑。
静态方法与实例方法
其实,我们需要做的只是区分JavaScript静态方法和实例方法。
静态方法 是定义在类上的方法,而不是在类的实例上,静态方法内部访问不到this
与实例变量。所以我们只能通过类来调用这些方法,而不能通过一个实例来调用
class MyClass {
static staticMethod() {
console.log('这是个静态方法');
}
}
MyClass.staticMethod();
const myInstance = new MyClass();
myInstance.staticMethod();
实例方法 是定义在类的原型上的方法,实例方法内可以访问对象的属性,也可以访问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('-'));
console.log(Array.prototype.join.call(arr, '-'));
上面的例子里,join
方法是数组的实例方法。实例方法可以直接在数组的实例上调用,也可以通过Array.prototype.join.call
的方式来调用,这俩本质上是一样的。唯一区别是Array.prototype.join.call
允许我们在任何类似数组的对象上调用这个方法,哪怕它不是一个真正的数组。
等等?我们可以在不是数组的值上调用join
?是的
const pseudoArray = { 0: 'one', 1: 'two', 2: 'three', length: 3 };
pseudoArray.join(',');
const result = Array.prototype.join.call(pseudoArray, ',');
console.log(result);
所以,在对象上调用实例方法,等同于按照这个对象的原型链一层一层向父类上找同名方法来调用。
基础类型的自动包装
虽然其他支持面向对象编程范式的语言也有类似行为,也就是对基本类型的自动包装和自动拆包,但为了百分百掌握JavaScript的行为与他们的异同,还是再来确定一遍吧
每当我们在基本类型值上(例如"hello"
或6
)上调用方法,JavaScript引擎都会先使用基本类型对应的包装类型对值进行包装,调用对应的方法,最后将包装对象丢掉还原基础类型。这是个引擎内部的隐式操作,所以我们没有任何的感知。
JavaScript对于以下的基本类型,都有对应的包装类型。可以通过typeof
操作结果是基本类型名还是object
来确认:
string
- String
number
- Number
boolean
- Boolean
symbol
- Object
bigint
- Object
让我们列一下他们基本类型对应包装类型的使用:
const primitiveString = "hello";
const objectString = new String("hello");
console.log(typeof primitiveString);
console.log(typeof objectString);
const primitiveNumber = 42;
const objectNumber = new Number(42);
console.log(typeof primitiveNumber);
console.log(typeof objectNumber);
const primitiveBoolean = true;
const objectBoolean = new Boolean(true);
console.log(typeof primitiveBoolean);
console.log(typeof objectBoolean);
const primitiveSymbol = Symbol("description");
const objectSymbol = Object(primitiveSymbol);
console.log(typeof primitiveSymbol);
console.log(typeof objectSymbol);
const primitiveBigInt = 123n;
const objectBigInt = Object(primitiveBigInt);
console.log(typeof primitiveBigInt);
console.log(typeof objectBigInt);
所以,在基本类型上调用方法,等同于创建这个基本类型对应的包装类型的对象并调用方法,最后拆包并返回原始类型的值。本质上还是调用了同类型包装行为创建的对象上的方法。
"str".toUpperCase();
(new String("str")).toUpperCase()
转自https://www.cnblogs.com/camwang/p/18259567 作者CamWang
该文章在 2024/7/24 16:28:51 编辑过