JavaScript 学习笔记 (三)
前言
本文是通过 廖雪峰的 JavaScript 教程 学习而来. 全文内容几乎与教程无差别, 即本人跟着教程文字和代码均手敲一边, 遂产生此笔记. 笔记文件太大, 遂拆分成多个部分, 这是第三部分.
4. 面向对象编程
JavaScript 中所有数据都能看作对象, 但这并不是面向对象开发. 面向对象开发有两个基本的概念:
- 类: 类是对象的类型模板, 例如定义
Student
类表示学生, 类本身是一种类型,Student
类表示学生, 但不特指某一个学生 - 实例: 实例是根据类创建的对象, 例如根据
Student
类创建对象xiaoming
、xiaowang
等实例, 每一个实例是类的一个具体实现, 他们全部属于Student
类
然而在 JavaScript 中并没有 Class
的概念, 不区分类和实例, 而是通过原型 ( prototype ) 来实现面向对象编程.
原型就是利用现有的对象为假象的类来实现创建对象, 例如通过如下的代码创建一个 xiaoming
对象:
1 | // Student 类, 其实是对象 |
xiaoming
是一个对象, 它的原型指向 Student
对象, 这便是 JavaScript 中的面向对象编程. xiaoming
有自己的 name
, 但是没有 height
和 run()
, 但它的原型 Student
中有, xiaoming
从原型 Student
中继承了 height
和 run()
, 因此 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 | // 原型对象 |
4.1 创建对象
JavaScript 中每个对象都会设置一个原型, 指向它的原型对象. 如果通过字面量形式 let obj = {}
创建一个对象, 它的原型是 null
.
1 | let obj = { |
当我们通过 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
定义了 indexOf
、 shift
等方法, 因此可以在所有的 Array 对象上调用.
当我们创建一个函数:
1 | function func() { |
它的原型链是:
flowchart BT
func[func]
Function[Function.prototype]
Object[Object.prototype]
null
func --> Function --> Object --> null
Function.prototype
定义了 apply()
方法, 因此任意函数都能调用 apply()
方法.
构造函数
除了直接用 {...}
方法创建一个对象以外, JavaScript 还可以使用构造函数创建对象, 它的用法是, 先定义一个构造函数:
1 | function Student(name) { |
这个函数本身是一个普通函数, 但是在 JavaScript 中, 使用关键字 new
来调用这个函数, 并返回一个对象:
1 | let xiaoming = new Student('小明'); |
注意 : 如果不写 new
, 这就是一个普通函数, 它返回 undefined
, 但是如果写了 new
, 它就变成一个构造函数, 它绑定的 this
指向新创建的对象, 并默认返回 this
, 也就是不需要 return this
, 这便是代码中有点看不懂为什么可以调用 xiaoming.name
的原因, 将返回值添加上:
1 | function Student(name) { |
如果你又创建了 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
也就是说, xiaoming
、xiaozhang
、xiaowang
的原型指向了 函数Student
的原型. 用 new Student()
创建的对象还从原型上获得了一个 constructor
属性, 它指向函数 Student
本身:
1 | console.log(xiaoming.constructor === Student.prototype.constructor); // 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
指向的对象是 xiaoming
、xiangwang
的原型对象 ( 某个对象 1 ) , 这个原型对象自己还有个属性 constructor
, 指向 Student
函数本身.
xiaoming
、 xiaowang
这些对象并没有 prototype
属性, 不过可以用 __proto__
这个不标准的方法查看.
注意 : 虽然 xiaoming
和 xiaowang
都继承自 Student
, 它们都继承了 hello()
方法, 但是它们两个的 hello()
方法不是同一个函数 ( 尽管它们的方法名和函数都相同 ) .
1 | console.log(xiaoming.name); // 小明 |
如果想要通过 new Student()
创建的对象都共享一个 hello()
方法节省内存, 根据对象的属性查找原则, 只需要把 hello()
方法放到 xiaoming
和 xiaowang
的原型对象上, 也就是让 某个对象 1
拥有 hello
方法即可. 而 Student.prototype
指向的就是 某个对象1
. 修改代码如下:
1 | // function Student(name) { |
避免忘记写 new
如果忘记写 new
, 也就是代码变成下面的样子, 会发生什么呢? 思考: 不用 new
情况下, Student
就是一个普通函数, 不再是一个对象构造函数, this
在绑定的是 undefined
, 为 undefined
绑定 name
属性将会报错.
1 | function Student(name) { |
因此, 千万不要 忘记给构造函数 new 关键字 , 一般为了区分普通函数和构造函数, 一般约定普通函数首字母小写, 构造函数首字母应当大写, 这样语法检测插件可以帮助检查漏写的 new
.
最后,如果写一个函数 createStudent()
便可以一劳永逸, 代码中只在 createStudent()
函数中写一次 new
关键字:
1 | function Student(props) { |
如果创建的对象有很多属性, 我们只需要传递某些属性, 剩下的用默认值, 由于参数是一个 Object, 如果从 JSON
拿到一个对象, 就可以直接创建出 xiaoming
.
4.2 原型继承–理解不动
在传统的基于 Class
的语言例如 Java、C++ 中, 继承的本质是扩展 Class
, 并生成新的 Subclass
. 由于这类语言严格区分类与实例, 继承实际上是类型的扩展, 但是 JavaScript 采用原型继承, 无法直接扩展一个 Class
, 因为根本没有 Class
这种类型.
那该怎么进行原型的继承呢, 先回顾 Student
构造函数:
1 | function Student(props) { |
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 | function PrimaryStudent(props) { |
但是调用了 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
定义的方法.
如果通过下面的方法进行设置是不行的, 因为 PrimaryStudent
和 Student
就共享了同一个原型对象, 那么 PrimaryStudent
就多余了.
1 | PrimaryStudent.prototype = Student.prototype; |
必须借助一个中间对象实现正确的原型链, 这个中间对象的原型要指向 Student.prototype
. 中间对象可以用一个空函数 Func
来实现:
1 | // PrimaryStudent |
新的原型链:
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 | // xiaoming 是通过 PrimaryStudent 构造函数创建的, 那么 xiaoming 的 __proto__ 指向了构造函数 PrimaryStudent 的 prototype 对象 |
4.4 __proto__ 与 prototype 区别
4.4.1 prototype
prototype
是构造函数 ( 大写首字母的函数, 要区分构造函数和构造方法 ) , 比如 Student()
的一个属性, 它用于定义构造函数创建的实例对象的共有方法和属性. 当使用 new
操作符创建一个实例对象时, 构造函数的 prototype
属性被赋值给实例对象的 __proto__
属性, 使得该对象可以继承构造函数的原型链上的方法.
4.4.2 __proto__
proto
是每一个 JavaScript 对象 ( 除了 null ) 都有的一个内部属性, 它指向该对象的构造函数的 prototype 对象, 从而形成原型链. 这个属性用于查找对象的属性或者方法, 当在当前对象上找不到属性或者方法, JavaScript 就会沿着 __proto__
指向的原型链向上查找, 直到找到为止. 例如:
1 | const person = new Person('alice'); |
这里 person.__proto__
指向 Person.prototype
, 因此 person
可以访问 Person.prototype
上的方法.
总结:
person.__proto__
是对象的属性, 指向创建该对象的构造函数的原型对象.prototype
形成原型链用于查找..prototype
是构造函数的属性, 创建实例时用于赋值给实例的.__proto__
, 等同于创建实例时执行了如下语句:1
2
3
4
5
6
7
8
9function 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
第三部分结束