调用了这么久的JS方法是长在对象、类、值本身还是原型链上?

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()本来也没想返回包装类型的对象

结语

哈哈,原来这个类、对象方法调用现象的原因其实一直都在我的大脑里,这只是面向对象编程中的一个很稀松平常的事实,但平时从来只是使用,还从来没联想过为何他会这样。

不知道你有没有感受到这种编程语言带来的实践经验与基础理论交融的乐趣。在一点点的实践中才会慢慢发现原来看似“这样写就能跑”的一些代码,其实背后都有曾经学习、分析过的程序概念和理论的支撑。这种感受或许就是编程快乐的其中之一个源头吧。

为大家的好奇心与耐心致敬。

调用了这么久的JS方法是长在对象、类、值本身还是原型链上?的更多相关文章

  1. JS 对象API之判断父对象是否在子对象的原型链上

    语法:父对象.prototype.isPrototypeOf(子对象) 代码栗子: function Student(){ this.name = "小马扎"; ; } var s ...

  2. 深入分析JS原型链以及为什么不能在原型链上使用对象

    在刚刚接触JS原型链的时候都会接触到一个熟悉的名词:prototype:如果你曾经深入过prototype,你会接触到另一个名词:__proto__(注意:两边各有两条下划线,不是一条).以下将会围绕 ...

  3. 【JS档案揭秘】第三集 深入最底层探秘原型链

    关于这部分我看过大量的文章,数不胜数,包括阮一峰的继承三部曲,还有各种慕课的视频教程,网上无数继承方法的对比.也对很多概念存在长期错误的理解.今天做一个正确的总结,用来给原型链和继承这块知识画上句号, ...

  4. js学习笔记之自调用函数、闭包、原型链

     自调用函数 var name = 'world!'; // console.log(typeof name) (function () { console.log(this.name, name, ...

  5. 在Array原型链上扩展remove,contain等方法所遇到的坑

    相信jser兄弟们肯定会碰到这样一个问题, 在做数组类的操作的时候,会要求删除数组中的一个元素:亦或是判断某值是否存在于这个数组: OK,拿删除数组元素举例,扩展方法为: Array.prototyp ...

  6. repr方法字符串输出实例对象的值

    #coding=utf-8 #repr方法字符串输出实例对象的值 class CountFromBy(object): def __init__(self, val=0, incr=1): self. ...

  7. JS 在open打开的子窗口页面中调用父窗口页面的JS方法

    需求的情景如下: 1:做新增或修改等操作的时候打开一个新的浏览器窗口(使用window.open(参数等)方法) 2:在新增或修改等的页面上有返回按钮.重置按钮.保存按钮,对于返回就直接关闭此窗口(使 ...

  8. JS中面向对象的,对象理解、构造函数、原型、原型链

    6.1 理解对象 6.1.1 对象属性类型 ECMS属性有两种类型:数据属性和访问器属性 1 数据属性 [[configurable]] 表示能否通过Delete 删除属性从而从新定义属性,能否修改属 ...

  9. 原型链上的call方法集合

    1. Object.prototype.toString.call(value) // 返回数据的类型 // "[object Object]" 等 2. Array.protot ...

  10. 实现 iframe 子页面调用父页面中的js方法

    父页面:index.html(使用iframe包含子页面child.html) [xhtml] view plaincopyprint? <html> <head> <s ...

随机推荐

  1. Dubbo 和 HSF 在阿里巴巴的实践:携手走向下一代云原生微服务

    ​简介: HSF 和 Dubbo 的融合是大势所趋.为了能更好的服务内外用户,也为了两个框架更好发展,Dubbo 3.0 和以 Dubbo 3.0 为内核适配集团内基础架构生态的 HSF 3 应运而生 ...

  2. [FAQ] PHP Warning: json_encode(): double INF does not conform to the JSON spec

    如果待 json 编码元素的数值趋近无穷大,会有这个提示. 比如:小数位超出长度. 解决方式建议保留固定长度的位数,也可以四舍五入. round(sprintf('%.11f', xxxxx), 10 ...

  3. [Trading] 人物: 陈向忠日内交易技术核心 - 趋势形态与成交量

    分时图判断趋势(开仓方向) 只要是低点不断抬高的,就是上涨趋势,高点是否提高是其次的. 只要是高点不断降低的那就是下降趋势,假如低点也在不断降低,那么这样的下降趋势就更加完美一些. 很多人就是看对了趋 ...

  4. dotnet DirectX 通过 Vortice 控制台使用 ID2D1DeviceContext 绘制画面

    在上一篇博客里面告诉大家,如何使用 Vortice 从零开始控制台创建 Direct2D1 窗口.上一篇博客采用的是 CreateDxgiSurfaceRenderTarget 的方式拿到了 ID2D ...

  5. dotnet 对指针转换为结构体多个不同方法的性能分析

    在 dotnet 里面,拿到一个指针,可以有多个不同的方法转换为结构体,本文将来告诉大家这几个方法的性能的差别 特别感谢性能优化狂魔 Stephen Toub 大佬的指导 在 WPF 框架开发中,有小 ...

  6. 18.基于Consul的服务发现和ConsulManager管理

    192.168.10.14 prometheus.consul 192.168.10.100 各类服务 一.基于Consul的服务发现 Consul 是由 HashiCorp 开发的一个支持多数据中心 ...

  7. 4.k8s-配置网络策略 NetworkPolicy

    一.基本了解 官方文档:https://kubernetes.io/zh-cn/docs/concepts/services-networking/network-policies/基本了解: 1.网 ...

  8. 六:大数据架构 - Flink + AI

    Flink 在AI 中的价值其实和大数据Lambda架构中流批统一这两个概念有关系,Flink为大数据实时化带来的价值也将同样使AI受益 大数据的发展过程 从Google奠基性的"三架马车& ...

  9. .NET静态代码织入——肉夹馍(Rougamo)发布3.0

    肉夹馍(https://github.com/inversionhourglass/Rougamo)通过静态代码织入方式实现AOP的组件,其主要特点是在编译时完成AOP代码织入,相比动态代理可以减少应 ...

  10. go1.18泛型全部教程

    目录 go1.18泛型全部教程 一 什么是泛型 二 Golang中的泛型 三 泛型语法详解 3.1 泛型的语法 3.2 Constraint(约束)是什么 3.3 自定义constraint(约束) ...