JavaScript - Object
OOP
实现方式
在不同的编程语言中,设计者利用各种不同的
语言特性
来抽象描述对象
- 最为成功的流派:使用
类
来描述对象
,典型代表为Java
、C++
JavaScript
的实现方式:原型
(更冷门!)
Like Java
- JavaScript 诞生之初
模仿 Java
,在原型运行时
引入了new
,this
等语言特性 - 在 ES6 之前,产生了很多『框架』:
试图在原型体系的基础上,把 JavaScript 变得更像是基于类的编程
- 这些『框架』最终成为了 JavaScript 的
古怪方言
- 这些『框架』最终成为了 JavaScript 的
任何语言在
运行时
,类
的概念都会被弱化
对象模型
基本特征
- 对象有
唯一标识性
:完全相同的两个对象,也并非同一个对象 - 对象有
状态
:同一对象可能处于不同的状态之下 - 对象有
行为
:对象的状态,可能因为它的行为产生变迁
对象的
唯一标识
,一般是通过内存地址
来体现的
1 | let a = {name: 'A'} |
状态
和行为
,不同语言会使用不同的术语来抽象描述
C++
:成员变量
和成员函数
Java
:属性
和方法
JavaScript
:统一抽象为属性
(包括函数)
1 | let x = { |
独有特征
JavaScript 对象具有
高度的动态性
:可以在运行时
修改对象的状态
和行为
在
运行时
,向对象添加属性
1 | let x = {name: "zhongmingmao"}; |
属性分类
JavaScript 提供:
数据属性
、访问器属性
(getter/setter)
JavaScript 用一组特征(
attribute
)来描述属性(property
)
数据属性
接近其他语言的属性概念
Attribute | Desc |
---|---|
value |
属性的值 - 常用 |
writable |
属性能否被赋值 |
enumerable |
fo...in 能否枚举 该属性 |
configurable |
属性能否被删除 或者改变 特征值 |
通常用于定义属性的代码会产生数据属性,其中
writable
、enumerable
和configurable
默认为true
1 | let x = { |
Object.defineProperty
:改变属性的特征
、定义访问器属性
1 | let x = { |
访问器属性
访问器属性使得属性
每次
在读
和写
时执行代码
,可以视为一种函数的语法糖
Attribute | Desc |
---|---|
getter |
函数 或者undefined ,在读取属性值 时被调用 |
setter |
函数 或者undefined ,在设置属性值 时被调用 |
enumerable |
fo...in 能否枚举 该属性 |
configurable |
属性能否被删除 或者改变 特征值 |
创建对象时,使用
get
和set
关键字来创建访问器属性
1 | let x = { |
JavaScript is OOP ?
- JavaScript 对象的运行时:
属性的集合
- 属性:以
String
或者Symbol
为 Key,以特征值(Attribute
)为 Value - 样例
- Key
name
- Value
{ value: "zhongmingmao", writable: true, enumerable: true, configurable: true }
- Key
- 属性:以
对象
是一个属性的索引结构
(Key-Value)- JavaScript 为
正统的 OOP 语言
- JavaScript 提供
完全运行时
的对象系统
(高度动态
),可以模仿
常见的面向对象范式
(基于类
+ 基于原型
)
- JavaScript 提供
编程范式
『基于类』
并非OOP
的唯一形态,原型系统
本身也是一个非常优秀的抽象对象
的形式
从 ES6 开始,JavaScript 提供了
class
关键字来定义类
(但本质仍然是基于原型运行时系统
的模拟
)
类 vs 原型
- 基于
类
的编程:提倡使用一个关注类与类之间关系
的开发模型 - 基于
原型
的编程:提倡关注一系列对象实例的行为
,然后才去关心如何将这些对象划分
到使用方式相似的原型对象
基于原型的 OOP 系统通过
『复制』
的方式来创建新对象
在 JavaScript 中,复制仅仅只是使得新对象持有一个原型的引用
JavaScript prototype
概述
- 所有对象都有
私有
字段prototype
,代表对象的原型 - 读取一个
属性
,如果对象本身没有,则会继续访问对象的原型
,直到原型为空或者找到为止
原型操作
从 ES6 之后,JavaScript 提供了一系列
内置函数
,使得可以更为方便地操作和访问原型
Function | Desc |
---|---|
Object.create |
根据指定的原型创建新对象,原型可以为 null |
Object.getPrototypeOf |
获得一个对象的原型 |
Object.setPrototypeOf |
设置一个对象的原型 |
1 | let cat = { |
早期版本
class
早期版本的 JavaScript 为内置类型指定了
class
属性,可以通过Object.prototype.toString
来访问
1 | let o = new Object; // [object Object] |
在 ES3 及之前的版本,
类
是一个很弱
的概念,仅仅只是运行时
的一个字符串属性
从
ES5
开始,class
私有属性被Symbol.toStringTag
代替
可以通过Symbol.toStringTag
来自定义Object.prototype.toString
的行为
1 | let x = { |
new
new 依然为 JavaScript OOP 的一部分
new 运算:接受
一个构造器
和一组调用参数
- 以
构造器的 prototype 属性
为原型
,创建新对象 - 将
this
(刚刚新建的对象) 和调用参数
传给构造器
执行 - 如果构造器返回的是对象,则返回;否则返回第1步创建的对象(默认
return this
)
试图让
函数对象
在语法上跟类
变得类似__proto__
是mozilla
提供的私有属性,多数环境不支持
1 | // 在构造器中添加属性 |
1 | // 在构造器的 prototype 属性(以此为原型创建对象)上添加属性 |
ES6 class
ES6 引进的
class
的特性,替代了原有的new + function
的怪异组合(但运行时并没有改变
)
使得function
回归原本的函数语义
ES6 引入了
class
关键字,在标准中删除
了所有[[class]]
相关的私有属性
类
的概念正式从属性
升级为语言的基础设施
,从此基于类的编程方式
正式成为了 JavaScript 的官方编程范式
类的写法本质上也是由
原型运行时
来承载的
逻辑上 JavaScript 认为每个类
是有共同原型的一组对象
,类中定义的方法和属性会被写在原型对象
之上
1 | class Rectangle { |
类提供了
继承
能力
1 | class Animal { |
对象分类
宿主对象
host object
:由 JavaScript宿主环境
提供的对象,对象的行为完全由宿主环境决定
JavaScript 常见的宿主环境为
浏览器
在浏览器环境中,有全局对象
window
(属性一部分来自于JavaScript 语言
,一部分来自于浏览器环境
)JavaScript 标准
中规定了全局对象属性
,w3c
的各种标准中规定了 window 对象的其它属性宿主对象也可以分为:
固有对象
+用户可创建对象
内置对象
build-in object
:由JavaScript
语言提供的对象
固有对象
intrinsic object
:由标准
规定,随着JavaScript 运行时
而自动创建
的对象实例
- 固有对象在
任何 JS 代码执行前
就已经被创建出来了,扮演『基础库』
的角色 ECMA
标准定义了150+
个固有对象
原生对象
native object
:可以通过JavaScript 语言本身的构造器
创建的对象
- 在 JavaScript 标准中,提供了
30+
构造器,可以通过new
运算符创建新的对象 - 基本上所有这些构造器的能力都是
无法通过纯 JavaScript 代码实现
,也无法用 class/extends 来继承
基本类型 | 基础功能和数据结构 | 错误类型 | 二进制类型 | 带类型的数组 |
---|---|---|---|---|
Boolean | Array | Error | ArrayBuffer | Float32Array |
String | Date | EvalError | SharedArrayBuffer | Float64Array |
Number | RegExp | RangeError | DataView | Int8Array |
Symbol | Promise | ReferenceError | Int16Array | |
Object | Proxy | SyntaxError | Int32Array | |
Map | TypeError | Uint8Array | ||
WeakMap | URIError | Uint16Array | ||
Set | Uint32Array | |||
WeakSet | Uint8ClampedArray | |||
Function |
通过这些构造器创建的对象多数使用了
私有字段
(无法通过原型继承
)
原生对象:为了特定能力或者性能
,而设计出来的特权对象
原生对象 | 私有字段 |
---|---|
Error | [[ErrorData]] |
Boolean | [[BooleanData]] |
Number | [[NumberData]] |
Date | [[DateValue]] |
RegExp | [[RegExpMatcher]] |
Symbol | [[SymbolData]] |
Map | [[MapData]] |
普通对象
ordinary object
:由{}
、Object 构造器
或者class
关键字定义类创建的对象,能够被原型继承
函数对象 vs 构造器对象
用
对象
来模拟
:函数
和构造器
- 定义
函数对象
:具有[[call]]
私有字段的对象构造器对象
:具有[[construct]]
私有字段的对象
- 使用
- 任何对象只要实现了
[[call]]
,就是一个函数对象
,可以作为函数被调用 - 任何对象只要实现了
[[construct]]
,就是一个构造器对象
,可以作为构造器被调用
- 任何对象只要实现了
- 用
function
关键字创建的函数必定同时
是函数
和构造器
对于
宿主对象
和内置对象
来说,在实现[[call]]
和[[construct]]
不总是一致的
1 | // Date 作为构造器被调用时,产生对象 |
1 | // 在浏览器宿主环境,Image 只能被当作构造器使用,而不允许作为函数使用 |
1 | // String Number Boolean 被当作函数使用时,会产生类型转换的效果 |
在 ES6 之后,
=>
创建的函数,仅仅只是函数
,无法被当作构造器来使用
1 | let f = () => { |
使用
function 语法
或者Function 构造器
创建的对象,[[call]]
和[[construct]]
是执行同一段代码
1 | function f() { |
[[construct]]
的执行过程
- 以
Object.protoype
为原型
创建一个新对象 - 以该新对象为
this
,执行函数[[call]]
- 如果
[[call]]
的返回值为对象,则返回这个对象;否则返回第 1 步创建的新对象(隐含reture this
)
1 | // 如果构造器返回了一个新对象 |