有点不同的面向对象
JavaScript采用基于原型的面向对象设计,这一选择既有历史因素也有技术优势。从语言诞生背景来看,JavaScript最初需要与Java形成差异化定位以避免直接竞争,因此刻意绕过了"类"的概念,转而通过原型机制实现继承关系。这种设计策略不仅实现了与Java的错位发展,更催生出独特的编程范式:
-
动态扩展能力
原型系统的核心优势在于其动态性------对象结构和继承关系在运行时仍可自由调整。开发者能够随时为原型添加新属性/方法,所有实例将自动继承这些变更。这种特性突破了传统类式语言(如Java)在编译时固化类结构的限制,使得功能扩展如同"动态捏塑"般灵活。虽然JavaScript本身采用单继承原型链,但通过原型组合(如
Object.assign()
)或混入模式,可以实现类似多继承的效果。 -
声明式语法与内存效率
通过构造函数创建对象时,只需用
new
关键字即可快速实例化,无需预先编写冗长的类定义。更关键的是,方法统一存储在构造函数的prototype
属性上,所有实例通过原型链共享同一份方法代码。相比每个实例单独存储方法的实现方式,这种设计显著减少了内存占用,在需要创建大量实例的场景下优势尤为明显。 -
原型即对象的统一模型
JavaScript将所有实体抽象为对象(包括函数、原型甚至构造函数本身),形成了高度自洽的编程模型。这种设计让继承关系不再依赖特定的类结构,而是通过原型链的动态链接实现。开发者可以直接操作原型对象,例如通过
Object.create()
基于现有对象创建新类型,或是动态修改__proto__
(现代更推荐用Object.setPrototypeOf()
)改变继承关系,展现出传统类式语言难以企及的灵活性。
这种基于原型的语言特性,使得JavaScript既能保持轻量简洁的语法形式,又能满足动态语言快速迭代的需求,最终成就了其在前端开发领域不可替代的地位。随着ES6引入class
语法糖,虽然表面上更接近传统面向对象范式,但其底层实现仍完全基于原型系统。
"类"的实现机制
构造函数与原型链模型
JavaScript 的"类"本质是构造函数与原型链的协同运作。实际上,构造函数本质上与普通函数并无二致,底层并没有进行任何特殊处理。它们之间的主要区别在于使用方式和语义约定:首字母大写的构造函数用于定义对象的结构和初始化逻辑,而普通函数(通常首字母小写)则用于实现具体的功能。
当使用new
调用时,引擎的执行步骤如下:
- 创建空对象
- 给对象的属性赋值,绑定原型链(
[[Prototype]]
指向构造函数的prototype) - 执行构造函数,将构造函数this指向新对象
- 根据构造函数返回值类型决定最终对象
代码示例:
javascript
// 定义一个Person对象的构造函数
function Person(name,age){
this.name = name
this.age = age
}
// 创建一个Person对象
const person1 = new Person("Jack",18) // Person {name:'Jack',age:'18'}
console.log(person1.constructor.prototype === Person.prototype) // true
console.log(person1.__proto__ === Person.prototype) // true
原型链的动态性是其核心特征。所有实例共享原型方法(如上例中的greet),修改原型会立即影响所有已存在实例。这种设计在内存效率(避免重复存储方法)与扩展性(动态添加功能)之间取得平衡,但也要求开发者谨慎操作原型。
语法糖Class
为了迎合大众的开发习惯,ES6中引入了class语法糖来实现和别的语言类似的面向对象实现过程,但是本质上还是基于原型链来实现,是原型系统的结构化表达:
javascript
class Person {
static count = 0 // 静态属性(构造函数属性)
constructor(name) {
this.name = name
Person.count++
}
greet() { // 自动存入Person.prototype
console.log(this.name)
}
}
// 等价于
function Person(name) { /*...*/ }
Person.count = 0
Person.prototype.greet = function() { /*...*/ }
类语法引入了几个重要改进:
- 规范了"类"的创建方法
- 强制使用
new
调用构造函数,避免全局污染 - 更直观的继承语法(extends/super)
- 静态属性声明和私有字段支持
但底层继承机制未变,通过Object.getPrototypeOf(Student) === Person
可验证原型链关系。
封装的实现机制
在类中,某些重要数据(如不希望被恶意篡改或仅供内部使用的数据)需要隐藏起来,以确保安全性。此时,我们只希望暴露获取这些数据的方法,或者限制对这些属性的修改,此时就需要将这个属性定义为私有,这也称为封装(隐藏内部实现)。
在 ES6 之前,JavaScript 并没有提供原生的私有属性定义方式,因此我们需要自行实现。一种常见的方法是利用闭包的特性来封装私有属性。代码示例:
javascript
function Person(id){
let _id = id // 使用"_" 来区分私有属性
this.getId = function(){
return _id
}
this.setId = function(value,authKey){
if(authKey === "auth123"){
_id = value // 如果授权密钥正确,更新私有属性
}
throw new Error("Invalid authKey!") // 如果授权密钥错误,抛出错误
}
}
到了 ES6,新出了Symbol类型,由于每个Symbol的值都是唯一的,并且需要获取Symbol值需要知道该Symbol的引用。利用这一点,也可以实现私有属性,但不能完全实现,外部通过getOwnPropertySymbols
还是可以访问到。代码示例:
javascript
function Person(id) {
const _id = Symbol('_id')
this[_id] = id;
this.getId = function () {
return this[_id];
};
this.setId = function (value, authKey) {
if (authKey === "auth123") {
this[_id] = value; // 如果授权密钥正确,更新私有属性
} else {
throw new Error("Invalid authKey!"); // 如果授权密钥错误,抛出错误
}
}
}
const p = new Person(10)
Object.getOwnPropertySymbols(p)[0]
再发展到ES2019,JS真正实现了原生的私有属性,可以使用class结合"#"关键字来定义一个私有属性,这样子外部是完全访问不到这个属性的。代码示例:
javascript
class Person {
// 使用 # 前缀定义私有属性
#_id;
constructor(id) {
this.#_id = id;
}
getId() {
return this.#_id;
}
setId(value, authKey) {
if (authKey === "auth123") {
this.#_id = value; // 如果授权密钥正确,更新私有属性
} else {
throw new Error("Invalid authKey!"); // 如果授权密钥错误,抛出错误
}
}
}
继承的实现机制
Javascript使用原型链进行继承,每个对象都有一个内部属性 [[Prototype]]
(可通过 __proto__
或 Object.getPrototypeOf()
访问),它指向另一个对象(即其原型)。当访问对象的属性或方法时,如果当前对象没有定义该成员,JavaScript 会沿着原型链向上查找,直到找到或到达 null
。
使用原型链模拟继承:
javascript
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
Object.setPrototypeOf(Student.prototype, Person.prototype);
const student = new Student("Alice", 10);
student.sayHi(); // "Hi, I'm Alice"