JavaScript 面向对象编程

JavaScript 用一种称为构建函数的特殊函数来定义对象和它们的特征。构建函数非常有用,因为很多情况下您不知道实际需要多少个对象(实例)。构建函数提供了创建您所需对象(实例)的有效方法,将对象的数据和特征函数按需联结至相应对象。

不像“经典”的面向对象的语言,从构建函数创建的新实例的特征并非全盘复制,而是通过一个叫做原形链的参考链链接过去的。

注: 一个构建函数通常是大写字母开头,这样便于区分构建函数和普通函数。

创建对象

参考写法

1
2
3
4
5
6
7
8
9
10
11
function Student(name) {
this.name = name;
}

Student.prototype.hello = function() {
alert('Hello, ' + this.name + '!');
};

let xiaoming = new Student('小明');
let xiaoming = new Student('小红');
xiaoming.hello === xiaohong.hello; // true,共享同一个函数

JS 对象通过原型链继承属性和方法。构造函数配合 new 创建实例,方法应放在 prototype 上共享。忘记 new 会出 bug,可用工厂函数封装。理解 constructorprototype__proto__ 三者的关系是掌握 JS 面向对象的关键。

原型继承

一、继承的本质

JavaScript 没有 Class(ES6 之前),继承通过修改原型链实现:

1
实例 → 子类原型 → 父类原型 → Object.prototype → null

二、为什么不能直接赋值原型

1
2
// ❌ 错误:共享同一个原型对象
PrimaryStudent.prototype = Student.prototype;

后果:子类和父类完全共享原型,修改子类会影响父类,失去继承意义。

三、正确做法:空函数桥接

1
2
3
4
5
function F() {}                    // 1. 创建空函数
F.prototype = Student.prototype; // 2. 空函数原型指向父类原型

PrimaryStudent.prototype = new F(); // 3. 子类原型 = 空函数实例
PrimaryStudent.prototype.constructor = PrimaryStudent; // 4. 修复 constructor

原理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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ES5 原型写法
function Student(name) {
this.name = name;
}
Student.prototype.hello = function() {
alert('Hello, ' + this.name + '!');
};

// ES6 class 写法(等价)
class Student {
constructor(name) {
this.name = name;
}

hello() {
alert('Hello, ' + this.name + '!');
}
}

对比优势

ES5 原型 ES6 class
构造函数 + prototype 分散定义 结构集中,语义清晰
继承需手写空函数桥接 extends 一行搞定
constructor 隐式处理 显式声明,可控

二、class 基本结构

1
2
3
4
5
6
7
8
9
10
class 类名 {
constructor(参数) {
// 构造函数,new 时自动调用
this.属性 = 值;
}

方法名() {
// 自动挂在 prototype 上
}
}

注意

  • 方法定义不需要 function 关键字
  • constructor 可省略(默认空构造)

三、extends 继承

1
2
3
4
5
6
7
8
9
10
class PrimaryStudent extends Student {
constructor(name, grade) {
super(name); // ✅ 必须先调用父类构造
this.grade = grade;
}

myGrade() {
alert('I am at grade ' + this.grade);
}
}

super 的两种用法

用法 场景 说明
super() 子类构造函数内 调用父类构造函数,必须在 this 之前
super.method() 子类方法内 调用父类的某个方法
1
2
3
4
5
6
class Dog extends Animal {
speak() {
super.speak(); // 调用父类的 speak()
console.log('Woof!');
}
}

四、继承验证

1
2
3
4
5
6
7
8
let xiaoming = new PrimaryStudent('小明', 2);

xiaoming.hello(); // "Hello, 小明!" — 继承自 Student
xiaoming.myGrade(); // "I am at grade 2" — 自有方法

xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true
xiaoming instanceof Object; // true

五、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
2
3
4
5
6
7
8
9
10
11
12
13
class Child extends Parent {
constructor() {
this.value = 1; // ❌ 错误!super() 之前不能用 this
super();
}
}

class Child extends Parent {
constructor() {
super(); // ✅ 正确:先 super(),再 this
this.value = 1;
}
}

七、一句话总结

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
2
3
4
5
6
7
8
9
10
11
12
13
function Person(first, last, age, gender, interests) {
// 属性与方法定义
}

var person1 = new Person("Tammi", "Smith", 32, "neutral", [
"music",
"skiing",
"kickboxing",
]);

Person.prototype.farewell = function () {
alert(this.name.first + " has left the building. Bye for now!");
};

JavaScript 中的继承

我们要做的第一件事是创建一个 Teacher() 构造器——将下面的代码加入到现有代码之下:

1
2
3
4
5
function Teacher(first, last, age, gender, interests, subject) {
Person.call(this, first, last, age, gender, interests);

this.subject = subject;
}

从无参构造函数继承

1
2
3
4
5
6
7
8
9
10
11
function Brick() {
this.width = 10;
this.height = 20;
}

function BlueGlassBrick() {
Brick.call(this);

this.opacity = 0.5;
this.color = "blue";
}

js 的原型式的继承

1
2
3
4
5
6
7
8
9
function Person(first, last, age, gender, interests) {
this.name = {
first,
last,
};
this.age = age;
this.gender = gender;
this.interests = interests;
}

设置 Student() 的原型和构造器引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Student class!
function Student(first, last, age, gender, interests) {
Person.call(this, first, last, age, gender, interests);
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

Student.prototype.greeting = function () {
alert("Yo! I'm " + this.name.first + ".");
};

let student1 = new Student("Liz", "Sheppard", 17, "female", [
"ninjitsu",
"air cadets",
]);

对象成员总结

总结一下,您应该基本了解了以下三种属性或者方法:

  1. 那些定义在构造器函数中的、用于给予对象实例的。这些都很容易发现 -
    在您自己的代码中,它们是构造函数中使用this.x = x类型的行;在内置的浏览器代码中,它们是可用于对象实例的成员(通常通过使用new关键字调用构造函数来创建,例如var myInstance = new myConstructor())。
  2. 那些直接在构造函数上定义、仅在构造函数上可用的。这些通常仅在内置的浏览器对象中可用,并通过被直接链接到构造函数而不是实例来识别。 例如[Object.keys()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/keys)
  3. 那些在构造函数原型上定义、由所有实例和对象类继承的。这些包括在构造函数的原型属性上定义的任何成员,如myConstructor.prototype.x()