JavaScript 面向对象编程
JavaScript 用一种称为构建函数的特殊函数来定义对象和它们的特征。构建函数非常有用,因为很多情况下您不知道实际需要多少个对象(实例)。构建函数提供了创建您所需对象(实例)的有效方法,将对象的数据和特征函数按需联结至相应对象。
不像“经典”的面向对象的语言,从构建函数创建的新实例的特征并非全盘复制,而是通过一个叫做原形链的参考链链接过去的。
注: 一个构建函数通常是大写字母开头,这样便于区分构建函数和普通函数。
创建对象
参考写法
1 | function Student(name) { |
JS 对象通过原型链继承属性和方法。构造函数配合 new 创建实例,方法应放在 prototype 上共享。忘记 new 会出 bug,可用工厂函数封装。理解 constructor、prototype、__proto__ 三者的关系是掌握 JS 面向对象的关键。
原型继承
一、继承的本质
JavaScript 没有 Class(ES6 之前),继承通过修改原型链实现:
1 | 实例 → 子类原型 → 父类原型 → Object.prototype → null |
二、为什么不能直接赋值原型
1 | // ❌ 错误:共享同一个原型对象 |
后果:子类和父类完全共享原型,修改子类会影响父类,失去继承意义。
三、正确做法:空函数桥接
1 | function F() {} // 1. 创建空函数 |
原理:new F() 创建的对象,其 __proto__ 指向 Student.prototype,实现了原型链的"搭桥"。
四、完整继承步骤
| 步骤 | 代码 | 作用 |
|---|---|---|
| 1. 借用父类构造函数 | Student.call(this, props) |
继承父类的实例属性 |
| 2. 定义子类自身属性 | this.grade = props.grade |
添加子类特有属性 |
| 3. 桥接原型链 | inherits(PrimaryStudent, Student) |
建立原型继承关系 |
| 4. 扩展子类方法 | PrimaryStudent.prototype.getGrade = ... |
在子类原型上定义新方法 |
ES6 class 继承
一、class 的本质
语法糖 —— 底层仍是原型继承,但写法更接近传统面向对象语言(Java/C++)。
1 | // ES5 原型写法 |
对比优势:
| ES5 原型 | ES6 class |
|---|---|
构造函数 + prototype 分散定义 |
结构集中,语义清晰 |
| 继承需手写空函数桥接 | extends 一行搞定 |
constructor 隐式处理 |
显式声明,可控 |
二、class 基本结构
1 | class 类名 { |
注意:
- 方法定义不需要
function关键字 constructor可省略(默认空构造)
三、extends 继承
1 | class PrimaryStudent extends Student { |
super 的两种用法
| 用法 | 场景 | 说明 |
|---|---|---|
super() |
子类构造函数内 | 调用父类构造函数,必须在 this 之前 |
super.method() |
子类方法内 | 调用父类的某个方法 |
1 | class Dog extends Animal { |
四、继承验证
1 | let xiaoming = new PrimaryStudent('小明', 2); |
五、ES5 vs ES6 继承对比
| 操作 | ES5 原型 | ES6 class |
|---|---|---|
| 定义类 | function Student() {} |
class Student {} |
| 定义方法 | Student.prototype.hello = ... |
写在 class 花括号内 |
| 继承 | 手写 inherits() 桥接 |
extends 关键字 |
| 调用父类构造 | Parent.call(this, ...) |
super(...) |
| 调用父类方法 | Parent.prototype.method.call(this) |
super.method() |
六、常见错误
1 | class Child extends Parent { |
七、一句话总结
class是 ES6 提供的语法糖,让原型继承写法更接近传统 OOP。核心三要素:class声明、constructor构造、extends+super()继承。底层仍是原型链,但再也不用写空函数桥接了。
额外
使用构造函数
Object()构造函数
首先, 您能使用[Object()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)构造函数来创建一个新对象。
是的, 一般对象都有构造函数,它创建了一个空的对象。
1 | var person1 = new Object(); |
使用 create()方法
1 | var person2 = Object.create(person1); |
您可以看到,person2 是基于 person1 创建的,它们具有相同的属性和方法。这非常有用,因为它允许您创建新的对象而无需定义构造函数。缺点是比起构造函数,浏览器在更晚的时候才支持create()方法(IE9, IE8 或甚至以前相比),加上一些人认为构造函数让您的代码看上去更整洁 —— 您可以在一个地方创建您的构造函数, 然后根据需要创建实例,这让您能很清楚地知道它们来自哪里。
注意:必须重申,原型链中的方法和属性没有被复制到其他对象——它们被访问需要通过前面所说的“原型链”的方式。
注意:没有官方的方法用于直接访问一个对象的原型对象——原型链中的“连接”被定义在一个内部属性中,在
JavaScript
语言标准中用[[prototype]]表示(参见 ECMAScript)。然而,大多数现代浏览器还是提供了一个名为[__proto__](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/proto)(前后各有
2
个下划线)的属性,其包含了对象的原型。你可以尝试输入person1.__proto__和person1.__proto__.__proto__,看看代码中的原型链是什么样的!
prototype 属性:继承成员被定义的地方
那么,那些继承的属性和方法在哪儿定义呢?如果你查看 [Object](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object) 参考页,会发现左侧列出许多属性和方法——大大超过我们在 person1 对象中看到的继承成员的数量。某些属性或方法被继承了,而另一些没有——为什么呢?
原因在于,继承的属性和方法是定义在 prototype 属性之上的(你可以称之为子命名空间
(sub namespace)
)——那些以 Object.prototype. 开头的属性,而非仅仅以 Object. 开头的属性。prototype 属性的值是一个对象,我们希望被原型链下游的对象继承的属性和方法,都被储存在其中。
修改原型
我们的代码中定义了构造器,然后用这个构造器创建了一个对象实例,此后向构造器的 prototype 添加了一个新的方法:
1 | function Person(first, last, age, gender, interests) { |
JavaScript 中的继承
我们要做的第一件事是创建一个 Teacher() 构造器——将下面的代码加入到现有代码之下:
1 | function Teacher(first, last, age, gender, interests, subject) { |
从无参构造函数继承
1 | function Brick() { |
js 的原型式的继承
1 | function Person(first, last, age, gender, interests) { |
设置 Student() 的原型和构造器引用
1 | // Student class! |
对象成员总结
总结一下,您应该基本了解了以下三种属性或者方法:
- 那些定义在构造器函数中的、用于给予对象实例的。这些都很容易发现 -
在您自己的代码中,它们是构造函数中使用this.x = x类型的行;在内置的浏览器代码中,它们是可用于对象实例的成员(通常通过使用new关键字调用构造函数来创建,例如var myInstance = new myConstructor())。 - 那些直接在构造函数上定义、仅在构造函数上可用的。这些通常仅在内置的浏览器对象中可用,并通过被直接链接到构造函数而不是实例来识别。 例如
[Object.keys()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/keys)。 - 那些在构造函数原型上定义、由所有实例和对象类继承的。这些包括在构造函数的原型属性上定义的任何成员,如
myConstructor.prototype.x()。