Class之"没落"
说起继承,由于之前学的是C和Java这些基于类的语言,我总能想到 class ,在JavaScript中,尽管类(class)自从ECMAScript 6(ES6)引入以来已经成为语言的一部分,但在某些情况下,你可能会发现它们使用得并不频繁。
-
历史原因:
- 函数原型继承:在ES6之前,JavaScript通过函数和原型链来实现继承和面向对象编程。许多开发者在ES6之前已经习惯了这种方法,并且已有大量的代码库是基于这种风格编写的。
- 类的概念是相对较新的:对于那些已经熟悉和习惯了函数原型继承的开发者,可能没有强烈的动机去改用
class。
-
灵活性和功能性:
- 函数和闭包:JavaScript的函数和闭包提供了很强的灵活性,可以实现类和对象的许多功能。例如,模块模式和工厂函数都是常见的设计模式,它们在很多情况下可以代替类。
- 原型继承:原型继承在某些场景下更加灵活,并且性能上可能更优一些,特别是在创建大量对象时。
-
简洁性和可读性:
- 简单的对象字面量:对于简单的对象结构,直接使用对象字面量(
{})比定义一个类要更简单、更直接。例如,配置对象和静态数据结构通常使用对象字面量。 - 小工具函数:对于一些简单的功能,定义一个工具函数(utility function)可能比定义一个类和实例化它要更简洁。
- 简单的对象字面量:对于简单的对象结构,直接使用对象字面量(
-
函数式编程的流行:
- 函数式编程范式:JavaScript也支持函数式编程范式,随着函数式编程的流行,许多开发者倾向于使用纯函数、不可变数据和高阶函数等函数式编程概念,这些在某种程度上减少了对类的使用。
很明显,函数比类更为的灵活且适用,所以呢 JavaScript 只有一种结构:对象。
原型链
每一个对象都有一个私有属性,指向另一个名为原型(prototype) 的对象
在设计模式牵手大模型文章中有浅浅提到原型和原型链,但还是属于理解,因此在这里详细理一下。
从官方文档中我们知道了。每一个对象都有一个私有属性指向原型,原型(prototype)也是一个对象。
prototype
- 定义:
prototype是每个函数(包括构造函数)独有的属性,它是一个对象。这个对象用于构造函数创建的所有实例对象共享的方法和属性。 - 作用: 当我们使用构造函数创建对象时,实例对象会继承构造函数的
prototype对象上的属性和方法。 - 访问方式: 通过构造函数可以访问
prototype属性:someConstructor.prototype
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log("Hello, " + this.name);
};
let alice = new Person("Alice");
alice.greet(); // 输出:Hello, Alice
在上述代码中,greet 方法被添加到 Person 构造函数的 prototype 上,因此所有使用 Person 构造函数创建的实例(如 alice)都可以访问这个方法。
那么我们再来看一个 __proto__(鄙人之前对二者理解地模糊不清)
__proto__
- 定义:
__proto__是每个对象(包括函数对象)都有的内部属性,它指向该对象的构造函数的prototype属性。 - 作用:
__proto__用于实现对象的原型链,它使得对象可以访问构造函数的prototype上的属性和方法。 - 访问方式: 通过实例对象可以访问
__proto__属性,例如:someObject.__proto__。
console.log(alice.__proto__ === Person.prototype); // 输出:true
在上述代码中,alice.__proto__ 指向 Person.prototype,这说明 alice 的原型是 Person.prototype。
不知道大家懂了没有,我们来看一个例子:
function doSomething() {}
console.log(doSomething.prototype);
输出
{
constructor: ƒ doSomething()
[[Prototype]]: Object
}
我们来分析一下
-
doSomething.prototype:- 当你定义一个函数时,JavaScript会自动给这个函数分配一个
prototype属性。这个属性是一个对象,默认情况下包含一个constructor属性,这个constructor属性指向函数本身。 - 因此,
doSomething.prototype默认是一个对象,它的constructor属性指向doSomething函数本身。
- 当你定义一个函数时,JavaScript会自动给这个函数分配一个
-
constructor:- 这个属性是指向创建该原型对象的函数,即这里的
doSomething函数。 - 在默认情况下,这个
constructor属性会自动添加到prototype对象中。
- 这个属性是指向创建该原型对象的函数,即这里的
-
[[Prototype]]:- 这表示
doSomething.prototype对象的原型,它是Object.prototype。这是因为所有的对象(除了通过Object.create(null)创建的对象)最终都继承自Object.prototype。
- 这表示
问题:doSomething.prototype对象的原型是如何与Object.prototype相关联的?
原型链和继承
在JavaScript中,对象是通过原型链来实现继承的。每个对象都有一个内部链接,称为[[Prototype]],指向另一个对象。如果对象在查找某个属性时找不到该属性,它会沿着这个链接去查找,直到找到该属性或到达原型链的末尾。
Function.prototype
当你定义一个函数,如doSomething时,这个函数对象会自动获得一个prototype属性,该属性是一个对象。这个对象有一个隐式的[[Prototype]]属性(可以用__proto__访问),指向Object.prototype。
因此才会有
constructor: doSomething,
[[Prototype]]: Object.prototype
图示化
doSomething (function)
|
|__ __proto__ --> Function.prototype
| |
| |__ __proto__ --> Object.prototype
| |
| |__ __proto__ --> null
|
|__ prototype (property)
|
|__ constructor: doSomething
|
|__ __proto__ --> Object.prototype
|
|__ hasOwnProperty
|__ toString
|__ ...
|__ __proto__ --> null
小结一下吧
-
__proto__存在于所有对象上,prototype只存在于函数上; -
每个对象都对应一个原型对象, 并从原型对象继承属性和方法, 该对应关系由
__proto__实现(访问对象内部属性[[Prototype]]); -
prototype用于存储共享的属性和方法, 其作用主要体现在 new 创建对象时, 为__proto__构建一个对应的原型对象(设置实例对象的内部属性[[Prototype]]); -
__proto__不是 ECMAScript 语法规范的标准, 是浏览器厂商实现的一种访问和修改对象内部属性[[Prototype]]的访问器属性(getter/setter), 现常用 ECMAScript 定义的Object.getPrototypeOf和Object.setPrototypeOf代替; -
prototype是 ECMAScript 语法规范的标准;
最后呢 我们再来看一个简单的例子,也是官方文档中的例子。
const o = {
a: 1,
b: 2,
// __proto__ 设置了 [[Prototype]]。它在这里被指定为另一个对象字面量。
__proto__: {
b: 3,
c: 4,
},
};
这里定义了一个对象 o,它有三个属性:
- `a`,值为 `1`
- `b`,值为 `2`
- `__proto__`,它是一个特殊属性,用于设置对象的原型(`[[Prototype]]`)。
在这个例子中,它被设置为另一个对象 `{ b: 3, c: 4 }`。
-
原型链
在这个例子中,o 的原型链如下:
o对象本身{ a: 1, b: 2 }o的[[Prototype]],即{ b: 3, c: 4 }o的[[Prototype]]的[[Prototype]],即Object.prototypeObject.prototype的[[Prototype]],即null
让我们一步步来看:
-
第一级:o 对象
o 对象自身有属性 a 和 b:
{
a: 1,
b: 2
}
-
第二级:o 的 [[Prototype]]
通过 __proto__,我们知道 o 的原型是这个对象:
{
b: 3,
c: 4
}
第三级:Object.prototype
{ b: 3, c: 4 } 对象的原型是 Object.prototype。这是 JavaScript 中所有普通对象的默认原型。
第四级:null
Object.prototype 的原型是 null,这是原型链的末端。
-
属性查找过程
当你访问 o 的属性时,JavaScript 引擎会按照原型链查找:
- 如果访问
o.a,它在o对象本身找到,值为1。 - 如果访问
o.b,它在o对象本身找到,值为2,所以不会查找原型中的b: 3。 - 如果访问
o.c,o对象本身没有c,于是会沿着原型链查找,在o.__proto__找到c: 4。
完整的原型链如下:
{ a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null