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 | // 如果构造器返回了一个新对象 |











