前言

本文是通过 廖雪峰的 JavaScript 教程 学习而来. 全文内容几乎与教程无差别, 即本人跟着教程文字和代码均手敲一边, 遂产生此笔记. 笔记文件太大, 遂拆分成多个部分, 这是第三部分.

4. 面向对象编程

JavaScript 中所有数据都能看作对象, 但这并不是面向对象开发. 面向对象开发有两个基本的概念:

  • 类: 类是对象的类型模板, 例如定义 Student 类表示学生, 类本身是一种类型, Student 类表示学生, 但不特指某一个学生
  • 实例: 实例是根据类创建的对象, 例如根据 Student 类创建对象 xiaomingxiaowang 等实例, 每一个实例是类的一个具体实现, 他们全部属于 Student

然而在 JavaScript 中并没有 Class 的概念, 不区分类和实例, 而是通过原型 ( prototype ) 来实现面向对象编程.

原型就是利用现有的对象为假象的类来实现创建对象, 例如通过如下的代码创建一个 xiaoming 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Student 类, 其实是对象
let Student = {
name: 'Robot',
height: 1.80,
run: function () {
console.log(`${this.name} is running`);
}
}
// 定义小明对象, 小明只有 name 属性
let xiaoming = {
name: '小明'
}
// 原型指向 Student 对象, 就好像 xiaoming 继承于 Student
xiaoming.__proto__ = Student;
console.log(xiaoming.name); // 小明
console.log(xiaoming.run()); // 小明 is running

xiaoming 是一个对象, 它的原型指向 Student 对象, 这便是 JavaScript 中的面向对象编程. xiaoming 有自己的 name, 但是没有 heightrun() , 但它的原型 Student 中有, xiaoming 从原型 Student 中继承了 heightrun() , 因此 xiaoming.run() 可以调用.

flowchart LR
	xiaoming["xiaoming<br>-name"]
	Student("Student<br>-name<br>-height<br>-run()")
	xiaoming-- prototype --> Student

JavaScript 的原型链和和 Java 的 Class 区别是, JavaScript 没有 Class 的概念, 所有对象都是实例, 所谓根据类创建对象, 不过就是将对象的原型指向 “类”, 类似继承一样.

在 JavaScript 运行过程中, 可以把对象的原型修改为另一个对象, 但是一般不这么做, 低版本浏览器无法使用 __prototype__ .

使用 Object.create() 方法, 传入一个原型对象, 创建一个新对象, 新对象没有任何属性. 可以使用这种方法创建 xiaoming:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 原型对象
let Student = {
name: 'Robot',
height: 1.80,
run: function () {
console.log(`${this.name} is running`);
}
}
// 创建对象函数
function createStudent (name) {
// 基于 Student 原型创建一个新对象
let s = Object.create(Student);
// 初始化对象
s.name = name;
return s;
}

let xiaoming = createStudent('小明');
console.log(xiaoming.run()); // 小明 is running
console.log(xiaoming.__proto__ === Student); // true

4.1 创建对象

JavaScript 中每个对象都会设置一个原型, 指向它的原型对象. 如果通过字面量形式 let obj = {} 创建一个对象, 它的原型是 null.

1
2
3
4
let obj = {
name: 'abc',
};
console.log(obj.__proto__); // [Object: null prototype] {}

当我们通过 obj.xxx 访问对象属性时, JavaScript 引擎先在当前对象上查找该属性, 若没有, 就到原型对象上找, 如果还没有有沿着原型链一直想上找到 Object.prototype 对象, 最后, 如果还是没有找到, 就只能返回 undefined.

例如, 创建一个 Array 对象:

1
let arr = [1, 2, 3, 4];

它的原型链是:

flowchart BT
	arr[arr]
	Array[Array.prototype]
	Object[Object.prototype]
	null
	arr --> Array --> Object --> null

Array.prototype 定义了 indexOfshift 等方法, 因此可以在所有的 Array 对象上调用.

当我们创建一个函数:

1
2
3
function func() {
return 1;
}

它的原型链是:

flowchart BT
	func[func]
	Function[Function.prototype]
	Object[Object.prototype]
	null
	func --> Function --> Object --> null

Function.prototype 定义了 apply() 方法, 因此任意函数都能调用 apply() 方法.

构造函数

除了直接用 {...} 方法创建一个对象以外, JavaScript 还可以使用构造函数创建对象, 它的用法是, 先定义一个构造函数:

1
2
3
4
5
6
function Student(name) {
this.name = name;
this.hello = () => {
alert(`Hello, ${this.name}`);
}
}

这个函数本身是一个普通函数, 但是在 JavaScript 中, 使用关键字 new 来调用这个函数, 并返回一个对象:

1
2
3
let xiaoming = new Student('小明');
console.log(xiaoming.name); // 小明
console.log(xiaoming.hello()); // Hello, 小明

注意 : 如果不写 new , 这就是一个普通函数, 它返回 undefined , 但是如果写了 new , 它就变成一个构造函数, 它绑定的 this 指向新创建的对象, 并默认返回 this , 也就是不需要 return this, 这便是代码中有点看不懂为什么可以调用 xiaoming.name 的原因, 将返回值添加上:

1
2
3
4
5
6
7
8
function Student(name) {
this.name = name;
this.hello = () => {
alert(`Hello, ${this.name}`);
}
// this 是一个对象
return this;
}

如果你又创建了 xiaowang , xiaozhang , 它们的原型链是:

flowchart BT
	xiaoming[xiaoming]
	xiaowang[xiaowang]
	xiaozhang[xiaozhang]
	Student[Student.prototype]
	Object[Object.prototype]
	null
	Student --> Object --> null
	xiaoming --> Student
	xiaowang --> Student
	xiaozhang --> Student

也就是说, xiaomingxiaozhangxiaowang 的原型指向了 函数Student 的原型. 用 new Student() 创建的对象还从原型上获得了一个 constructor 属性, 它指向函数 Student 本身:

1
2
3
4
console.log(xiaoming.constructor === Student.prototype.constructor); // true
console.log(Student.prototype.constructor === Student); // true
console.log(Object.getPrototypeOf(xiaoming) === Student.prototype); // true
console.log(xiaoming instanceof Student); // true

复杂的关系图如下:

flowchart LR
	xiaoming
	 %% 主线
    xiaoming --> Obj1 --> Obj2 --> null
	Obj2[某个对象]
    %% 某个对象1
    subgraph Obj1[某个对象]
        %% 内层框
        subgraph constructor1[-constructor]
        end
    end
    %% Student[Student]
    subgraph Student[Student]
        %% 内层框
        subgraph prototype1[-prototype]
        end
    end
    %% Object
    subgraph Object[Object]
        %% 内层框
        subgraph prototype2[-prototype]
        end
    end
    %% 连接关系
    constructor1 --> Student
    prototype1 --> Obj1
    prototype2 --> Obj2

红色箭头是原型, 注意 : Student.prototype 指向的对象是 xiaomingxiangwang 的原型对象 ( 某个对象 1 ) , 这个原型对象自己还有个属性 constructor , 指向 Student 函数本身.

xiaomingxiaowang 这些对象并没有 prototype 属性, 不过可以用 __proto__ 这个不标准的方法查看.

注意 : 虽然 xiaomingxiaowang 都继承自 Student , 它们都继承了 hello() 方法, 但是它们两个的 hello() 方法不是同一个函数 ( 尽管它们的方法名和函数都相同 ) .

1
2
3
console.log(xiaoming.name); // 小明
console.log(xiaowang.name); // 小王
console.log(xiaoming.hello === xiaowang.hello); // false

如果想要通过 new Student() 创建的对象都共享一个 hello() 方法节省内存, 根据对象的属性查找原则, 只需要把 hello() 方法放到 xiaomingxiaowang 的原型对象上, 也就是让 某个对象 1 拥有 hello 方法即可. 而 Student.prototype 指向的就是 某个对象1 . 修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// function Student(name) {
// this.name = name;
// this.hello = () => {
// console.log(`Hello, ${this.name}`);
// };
// }
function Student(name) {
this.name = name;
}
// 某个对象 1
Student.prototype.hello = function () {
alert(`Hello, ${this.name}!`);
};

避免忘记写 new

如果忘记写 new , 也就是代码变成下面的样子, 会发生什么呢? 思考: 不用 new 情况下, Student 就是一个普通函数, 不再是一个对象构造函数, this 在绑定的是 undefined , 为 undefined 绑定 name 属性将会报错.

1
2
3
4
5
6
7
8
9
function Student(name) {
this.name = name;
this.hello = () => {
console.log(`Hello, ${this.name}`);
};
}

let xiaoming = Student('小明');
console.log(xiaoming.name); // TypeError: Cannot read properties of undefined (reading 'name')

因此, 千万不要 忘记给构造函数 new 关键字 , 一般为了区分普通函数和构造函数, 一般约定普通函数首字母小写, 构造函数首字母应当大写, 这样语法检测插件可以帮助检查漏写的 new .

最后,如果写一个函数 createStudent() 便可以一劳永逸, 代码中只在 createStudent() 函数中写一次 new 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Student(props) {
this.name = props.name;
this.age = props.age;
}

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

// 一劳永逸之法
function createStudent(props) {
return new Student(props || {})
}

let xiaoming = createStudent({
name: 'xiaoming'
});
console.log(xiaoming.age);

如果创建的对象有很多属性, 我们只需要传递某些属性, 剩下的用默认值, 由于参数是一个 Object, 如果从 JSON 拿到一个对象, 就可以直接创建出 xiaoming.

4.2 原型继承–理解不动

在传统的基于 Class 的语言例如 Java、C++ 中, 继承的本质是扩展 Class , 并生成新的 Subclass. 由于这类语言严格区分类与实例, 继承实际上是类型的扩展, 但是 JavaScript 采用原型继承, 无法直接扩展一个 Class , 因为根本没有 Class 这种类型.

那该怎么进行原型的继承呢, 先回顾 Student 构造函数:

1
2
3
4
5
6
7
function Student(props) {
this.name = props.name || 'Unnamed';
}

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

Student 的原型链:

flowchart LR
	xiaoming
	xiaowang
	Obj2[某个对象2]
    %% 某个对象1
    subgraph Obj1[某个对象1]
        %% 内层框
        subgraph constructor1[-constructor]
        end
    end
    %% Student[Student]
    subgraph Student[Student]
        %% 内层框
        subgraph prototype1[-prototype]
        end
        subgraph hellO1[hello]
        end
    end
    %% Object
    subgraph Object[Object]
        %% 内层框
        subgraph prototype2[-prototype]
        end
    end
    %% 主线
    xiaoming --> Obj1
    xiaowang --> Obj1
    Obj1 --> Obj2
    Obj2 --> null
    %% 连接关系
    constructor1 --> Student
    prototype1 --> Obj1
    prototype2 --> Obj2
    %% 设置红色样式的其他连接线
    linkStyle 0 stroke:#ff0000,stroke-width:2px
    linkStyle 1 stroke:#ff0000,stroke-width:2px
    linkStyle 3 stroke:#ff0000,stroke-width:2px
    linkStyle 4 stroke:#ff0000,stroke-width:2px

现在, 基于 Student 扩展出 PrimaryStudent , 可以先定义 PrimaryStudent :

1
2
3
4
5
function PrimaryStudent(props) {
Student.call(this, props); // 调用 Student 构造函数, 绑定 this 变量
this.age = props.age || 18;
}
let ps = new PrimaryStudent({age: 19});

但是调用了 Student 方法并不是继承了 Student, 此时 PrimaryStudent 创建的对象原型是:

flowchart BT
	PS["new PrimaryStudent()"]
	PSP[PrimaryStudent.prototype]
	OP[Object.prototype]
	null
	PS --> PSP --> OP --> null

必须想办法把原型链修改成:

flowchart BT
	PS["new PrimaryStudent()"]
	PSP[PrimaryStudent.prototype]
	SP[Student.prototype]
	OP[Object.prototype]
	null
	PS --> PSP --> SP --> OP --> null

这样继承关系才是对的, 因为 PrimaryStudent 的原型是 Student.prototype . 新的基于 PrimaryStudent 创建的对象不但可以调用 PrimaryStudent.prototype 定义的方法也可以调用 Student.prototype 定义的方法.

如果通过下面的方法进行设置是不行的, 因为 PrimaryStudentStudent 就共享了同一个原型对象, 那么 PrimaryStudent 就多余了.

1
PrimaryStudent.prototype = Student.prototype;

必须借助一个中间对象实现正确的原型链, 这个中间对象的原型要指向 Student.prototype . 中间对象可以用一个空函数 Func 来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// PrimaryStudent
function PrimaryStudent(props) {
Student.call(this, props); // 调用 Student 构造函数, 绑定 this 变量
this.age = props.age || 18;
}

// 空函数
function Func() {}
// Func() 的原型指向 Student.prototype
Func.prototype = Student.prototype;
let F = new Func();
// PrimaryStudent 的原型指向一个新的 Func 对象 F, F 的原型刚好指向 Student.prototype
PrimaryStudent.prototype = F;
// PrimaryStudent 的原型构造函数修复为 PrimaryStudent
PrimaryStudent.prototype.constructor = PrimaryStudent;
// 在 PrimaryStudent 原型(就是 Func() 上)定义方法:
PrimaryStudent.prototype.getAge = function () {
return this.age;
};

let xiaoming = new PrimaryStudent({ name: '小明', age: 23 });
console.log(xiaoming.name); // 小明
console.log(xiaoming.getAge()); // 23

新的原型链:

flowchart LR
	xiaoming["xiaoming\n- name\n- age"]
	%% PrimaryStudent
	subgraph PS[PrimaryStudent]
		subgraph PSpt[- prototype]
		end
	end
	%% F
	subgraph F[F]
		subgraph Fct[- constructor]
		end
		subgraph gA[- getAge]
		end
	end
	%% 原型对象 1
	subgraph yx1[原型对象 1]
		subgraph yx1ct[- constructor]
		end
		subgraph yx1he[- hello]
		end
	end
	%% 原型对象 2
	yx2[原型对象 2]
	%% Student
	subgraph st[Student]
		subgraph stpt[- prototype]
		end
	end
	%% Obj
	subgraph obj[Object]
		subgraph objpt[- prototype]
		end
	end
	%% 主线
	xiaoming --> F
    F --> yx1
    yx1 --> yx2
    yx2 --> null
	%% 支线
	Fct --> PS
	PSpt --> F
	stpt --> yx1
	yx1ct --> st
	objpt --> yx2
	%% 颜色
	linkStyle 0 stroke:#ff0000,stroke-width:2px
    linkStyle 1 stroke:#ff0000,stroke-width:2px
    linkStyle 2 stroke:#ff0000,stroke-width:2px
    linkStyle 3 stroke:#ff0000,stroke-width:2px

理解 :

1
2
3
4
5
6
7
8
9
10
11
12
13
// xiaoming 是通过 PrimaryStudent 构造函数创建的, 那么 xiaoming 的 __proto__ 指向了构造函数 PrimaryStudent 的 prototype 对象
xiaoming.__proto__ === PrimaryStudent.prototype
// 这里通过 PrimaryStudent.prototype = new F() 就是将 xiaoming 的 __proto__ 指向了 new F()
PrimaryStudent.prototype = new F();
// 而 F.prototype = Student.prototype 就是将 F 本来指向的某个原型对象切换到了指向 Student 指向的原型对象 ( 这里是因为每个对象都有一个原型对象, F() 和 Student() 本不指向同一个对象, 修改之后便都指向了 Student 的原型对象--图中的原型对象1, Object 指向原型对象定义为 原型对象2 )
F.prototype = Student.prototype;
// 因此 xiaoming 便指向了 PrimaryStudent 的原型对象 F() ( xiaoming.__proto__ -> F() )
xiaoming.__proto__ = F()
// F() 原型对象指向了 Student 的原型对象 ( F.prototype -> Student.prototype )
xiaoming.__proto__ ->
// Student 原型对象 由于 JavaScript 特性自然指向了 Object 的原型对象 ( Student.prototype -> Object.prototype )
// Object 上面没有原型对象, 所以是 null ( Object.prototype -> null )

4.4 __proto__ 与 prototype 区别

4.4.1 prototype

prototype 是构造函数 ( 大写首字母的函数, 要区分构造函数和构造方法 ) , 比如 Student() 的一个属性, 它用于定义构造函数创建的实例对象的共有方法和属性. 当使用 new 操作符创建一个实例对象时, 构造函数的 prototype 属性被赋值给实例对象的 __proto__ 属性, 使得该对象可以继承构造函数的原型链上的方法.

4.4.2 __proto__

proto 是每一个 JavaScript 对象 ( 除了 null ) 都有的一个内部属性, 它指向该对象的构造函数的 prototype 对象, 从而形成原型链. 这个属性用于查找对象的属性或者方法, 当在当前对象上找不到属性或者方法, JavaScript 就会沿着 __proto__ 指向的原型链向上查找, 直到找到为止. 例如:

1
2
const person = new Person('alice');
console.log(person.__proto__ === Person.prototype);

这里 person.__proto__ 指向 Person.prototype , 因此 person 可以访问 Person.prototype上的方法.

总结:

  • person.__proto__ 是对象的属性, 指向创建该对象的构造函数的原型对象 .prototype 形成原型链用于查找.

  • .prototype 是构造函数的属性, 创建实例时用于赋值给实例的 .__proto__, 等同于创建实例时执行了如下语句:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function Person(props) {
    this.name = props.name || 'Unnamed';
    }
    let xiaoming = new Person({ name: '小明' });
    console.log(xiaoming.name);
    // 不执行自己下面写的语句之前先测试一下
    console.log(xiaoming.__proto__ === Person.prototype); // true, 就是说确实默认执行了下面的语句
    // 创建时便执行了下面的语句
    xiaoming.__proto__ = Person.prototype;

5. 浏览器

6. 错误处理

7. jQuery

8. underscore

9. Node.js

第三部分结束