理解Javascript面向对象

有点不同的面向对象

JavaScript采用基于原型的面向对象设计,这一选择既有历史因素也有技术优势。从语言诞生背景来看,JavaScript最初需要与Java形成差异化定位以避免直接竞争,因此刻意绕过了"类"的概念,转而通过原型机制实现继承关系。这种设计策略不仅实现了与Java的错位发展,更催生出独特的编程范式:

  1. 动态扩展能力

    原型系统的核心优势在于其动态性------对象结构和继承关系在运行时仍可自由调整。开发者能够随时为原型添加新属性/方法,所有实例将自动继承这些变更。这种特性突破了传统类式语言(如Java)在编译时固化类结构的限制,使得功能扩展如同"动态捏塑"般灵活。虽然JavaScript本身采用单继承原型链,但通过原型组合(如Object.assign())或混入模式,可以实现类似多继承的效果。

  2. 声明式语法与内存效率

    通过构造函数创建对象时,只需用new关键字即可快速实例化,无需预先编写冗长的类定义。更关键的是,方法统一存储在构造函数的prototype属性上,所有实例通过原型链共享同一份方法代码。相比每个实例单独存储方法的实现方式,这种设计显著减少了内存占用,在需要创建大量实例的场景下优势尤为明显。

  3. 原型即对象的统一模型

    JavaScript将所有实体抽象为对象(包括函数、原型甚至构造函数本身),形成了高度自洽的编程模型。这种设计让继承关系不再依赖特定的类结构,而是通过原型链的动态链接实现。开发者可以直接操作原型对象,例如通过Object.create()基于现有对象创建新类型,或是动态修改__proto__(现代更推荐用Object.setPrototypeOf())改变继承关系,展现出传统类式语言难以企及的灵活性。

这种基于原型的语言特性,使得JavaScript既能保持轻量简洁的语法形式,又能满足动态语言快速迭代的需求,最终成就了其在前端开发领域不可替代的地位。随着ES6引入class语法糖,虽然表面上更接近传统面向对象范式,但其底层实现仍完全基于原型系统。

"类"的实现机制

构造函数与原型链模型

JavaScript 的"类"本质是构造函数与原型链的协同运作。实际上,构造函数本质上与普通函数并无二致,底层并没有进行任何特殊处理。它们之间的主要区别在于使用方式和语义约定:首字母大写的构造函数用于定义对象的结构和初始化逻辑,而普通函数(通常首字母小写)则用于实现具体的功能。

当使用new调用时,引擎的执行步骤如下:

  1. 创建空对象
  2. 给对象的属性赋值,绑定原型链([[Prototype]]指向构造函数的prototype)
  3. 执行构造函数,将构造函数this指向新对象
  4. 根据构造函数返回值类型决定最终对象

代码示例:

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() { /*...*/ }

类语法引入了几个重要改进:

  1. 规范了"类"的创建方法
  2. 强制使用new调用构造函数,避免全局污染
  3. 更直观的继承语法(extends/super)
  4. 静态属性声明和私有字段支持

但底层继承机制未变,通过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"
相关推荐
大杯咖啡3 分钟前
localStorage与sessionStorage的区别
前端·javascript
RaidenLiu15 分钟前
告别陷阱:精通Flutter Signals的生命周期、高级API与调试之道
前端·flutter·前端框架
非凡ghost15 分钟前
HWiNFO(专业系统信息检测工具)
前端·javascript·后端
非凡ghost18 分钟前
FireAlpaca(免费数字绘图软件)
前端·javascript·后端
非凡ghost24 分钟前
Sucrose Wallpaper Engine(动态壁纸管理工具)
前端·javascript·后端
拉不动的猪26 分钟前
为什么不建议项目里用延时器作为规定时间内的业务操作
前端·javascript·vue.js
该用户已不存在33 分钟前
Gemini CLI 扩展,把Nano Banana 搬到终端
前端·后端·ai编程
地方地方34 分钟前
前端踩坑记:解决图片与 Div 换行间隙的隐藏元凶
前端·javascript
炒米233337 分钟前
【Array】数组的方法
javascript
小猫由里香40 分钟前
小程序打开文件(文件流、地址链接)封装
前端