重学 JavaScript01:就从面向对象说起吧
Macmillan的解释是:
a thing that you can see and touch that is not alive and is usually solid
一种你能够触摸到的实物,并非活物。
新华字典中,对象的基本含义是:
行动或思考时作为目标的人或事物
特指恋爱的对方
“你是我的研究对象,你是我的学习对象,这是今天的讨论对象”但如果单独用,就成了恋爱对方。“这是我对象”
因此对象在中文世界中并非像英文所指的那么普世,于是对象成了计算机领域中的专有名词,代表一切事物的总称。
而从英文解释中,可以体会到,object是人类认知世界,而产生的一种思维抽象。什么意思呢?也就是人类成长过程中,对眼前事物的感知。
我是两个孩子的爸爸,我认真观察过他们,他俩小时候都是先用眼睛看,进入口欲期后用嘴巴感知,这个东西是红色,那个东西冰冰凉,再然后大人们教他们数数,OK,这是两个苹果。于是对象有了属性和值。
《面向对象分析与设计中》,Grady Booch 说,从人类认知角度来说,对象是:
一个可以触摸或者可以看见的东西;
人的智力可以理解的东西;
可以指导思考或行动(进行想象或施加动作)的东西。
对象的特征是:
对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
对象有状态:对象具有状态,同一对象可能处于不同状态之下。
对象具有行为:即对象的状态,可能因为它的行为产生变迁。
计算机中的OBJECT
在计算机语言的设计中,不同的设计者利用着他们对object理解,来对object进行了描述,也就是我们今天耳熟能详的,比如Java、C++ 中基于“类”的面相对象编程的概念。
但Javascript有点特立独行,使用了“原形(Prototype)”。
the first form of something new, made before it is produced in large quantities
这里先不展开。(TODO)
JAVASCRIPT 的OBJECT 之路
由于产生之时的政治原因,JS受管理层要求模仿Java,于是你看到了创始人 Brendan Eich 在“原型运行时”的基础上引入了 new、this 等语言特性,使之“看起来更像 Java”。ES6规范之前,甚至有大量的框架将JS改造成基于类来编程,而这样做的弊端明显要大于收益。
JavaScript 的对象特征
幸运的是,任何语言的运行时,类的概念都会被弱化。
对照上面说的,面向对象的三个特征:唯一、状态、行为
唯一标识性
一般,对所有的语言来说,对象的唯一标识性都是通过内存地址来实现的,对象具有唯一标识的内存地址,也就具有唯一标识了。
也就是为什么,下面的返回为false:
状态与行为
对不同的语言来说,状态和行为会使用不同的描述:
C++ 中的“成员变量”和“成员函数”
Java中的“属性”和“方法”
而在JS中,状态和行为统一抽象成了属性(函数也是一种特殊的对象)。
如下例中,对对象o来说,d 和 f 就是两个普通的属性
也就是说,JS能很好地体现对象的基本特征。
JS对象的独有特色
但为什么很多人说“JS不是面向对象”的呢?因为JS的对象设计有其独有的特点,那就是:
对象具有高度的动态性,因为JS能在运行时被修改状态和行为。
你试过Java在运行时添加向对象添加属性吗?JS 能做到!
JS的属性
JS为了提高抽象能力,将属性设计成了(比别的语言)更加复杂的形势,提供了数据属性和访问器属性两类。也就是说,JS的属性并非简单的名称(键)和值,而是用了一组attribute(特征)来描述property(属性)。
数据属性
数据属性类似其他语言的“属性”,具有四个特征。
属性的值:value
能否赋值:writable
能否被(如for in)枚举:enumerable
能否被更新(改变或删除):configurable
虽然,我们通常只关系值。
访问器属性
也就是getter/setter属性,它也有四个特征。
取值时调用:getter
设置时调用:setter
能否被(如for in)枚举:enumerable
能否被更新(改变或删除):configurable
也就是说,读和写的时候,访问器属性可以执行代码,让读和写得到完全不同的值,可以看作一种函数的语法糖。
实践一下
这位客官问了,我怎么没设置过什么writable、enumerable、configurable啊?因为通常我们定义属性时,其默认值都是true,那我们怎么看到呢?getOwnPropertyDescripter
那如何改变呢?defineProperty
那访问器属性呢?
每次访问属性都会实行getter 或者setter 函数,因此o.a 每次都得到1。
总结一下, JS中的对象,实际上是一个“属性的集合”:
属性的key 为字符串或Symbol,
属性的value 为数据属性特征值或者访问器属性特征值
上面例子中的的a就是key,而它的value,是{writable:true,value:1,configurable:true,enumerable:true}
因此,JS的对象与其他语言相比,有些另类。
特殊却普通
正如我们前面说到的:
任何语言的运行时,类的概念都会被弱化。
因此JS可以模仿多数面向对象编程范式,比如基于原型,甚至基于类。也正因此,JS是正如其语言标准所说的:JS是一门面向对象的语言。
既然这样,我们就不要机械地用JS来模仿其他语言,充分利用它的高度动态性的属性集合这一对象系统,挖掘它的能力吧。
JAVASCRIPT 的面向对象
前面提到了,由于政治原因,JS推出就要求模仿Java, 所以创始人Brendan 在“原型运行时”的基础上引入了new、this等特性。但是,JS缺少了继承等关键特性,导致那个时候社区产生了许多互不兼容的解决方案。直到ES6的推出,JS提供了class 关键字,虽然还是基于原型,但统一了社区方案。
问题来了,基于原型和基于类貌似都可以是面向对象的一种型态。
“基于类”提倡的是关注类和类之间关系。套路是先有类,再去实例化对象。类与类之间可能继承、组合,类又与语言的类型系统整合,形成一定的编译时能力。
我们先假设这个世界上没有类。
什么是原型?
你应该听过“照猫画虎”,猫就被看做了老虎的原型。
“基于原型”符合我们对世界的认知,套路是工程师关注一系列对象实例的行为,然后再关心如何将这些对象,划分到使用方式相似的原型对象,而非将它们分成类。基于原型的对象系统是通过“复制”来创建新对象的。这当中有两种复制思路:
新对象持有一个原型的引用
完全复制一个对象,新老没有关联
JavaScript的原型
原型系统相当简单,两条原则:
如果所有对象都有私有字段[[prototype]],就是对象的原型
读取一个属性,如果对象本身没有,就会继续访问对象的原型,直到原型为空,或者找到为止。
ES6 为了更直接地操纵原型,提供了一系列内置函数,三个方法:
Object.create 根据指定的原型创建新对象,原型可以是 null;
Object.getPrototypeOf 获得一个对象的原型;
Object.setPrototypeOf 设置一个对象的原型。
然后我们利用这几个方法实现一下照猫画虎:
从ES6回到远古
回到模仿Java那时,早期的JavaScript中,“类”的定义是一个私有属性[[class]],内置类型如Number、String、Date等指定了[[class]]属性,唯一可以访问的方式是Object.prototype.toString:
ES5开始,[[class]] 私有属性被 Symbol.toStringTag 代替,甚至可以改变toString的行为(字符串加法可以触发toString 的调用):
JS中的new 到底做了哪些事情呢?
new 运算接受一个构造器和一组调用参数,做了以下几件事:
以构造器的prototype属性为原型,创建新对象
将this 和调用参数传递给构造器,执行
如果构造器返回的是对象,则返回,否则返回第一步创建的对象
也就是说,new 客观上提供了两种方式
在构造器中添加属性
在构造器的prototype属性上添加属性
早期版本中,没有Object.create 和 Object.setPrototypeOf,new 是唯一一个可以指定[[prototype]] 的方法(mozilla提供过私有属性__proto__,但是大多数环境不支持)。所以,当时就有人把new 来作为Object.create 使用
但这样有个问题,它不支持第二个参数,不支持null作为原型,因此意义已经不大了,算是当时的一种临时解决方案。
回到ES6中的类
ES6中终于加入了新特性class, new 与 function 搭配的怪异代码终于没了,任何情况下,都推荐使用ES6的语法定义类,而让function回归函数的语义。
get/set 关键字创建getter/setter
数据型成员最好写在构造器中
括号和大括号来创建方法
类的写法实际上也是由原型运行时来承载的,逻辑上JS认为每一个类是有共同原型的一组对象,类中定义的方法和属性则会被写在原型对象上。
类提供了继承能力
与早期的原型模拟方式相比,extends关键字自动设置了constructor, 并自动调用父类的构造函数,这种设计坑更少。
新世界中, class 关键字箭头运算符可以完全替代旧的function关键字,更明确地区分了定义函数和定义类两种意图。
我们不再需要用function关键字来模拟类了,有了正式的新语法,原型体系同时作为一种编程范式和运行时机制存在。但理解运行时的原型系统是十分必要的。
JAVASCRIPT 中的对象
我们所创建对象的能力是十分有限的:比如原生数组底层实现的随下标变化的length属性,比如浏览器中只能靠document.createElement创建div对象,也就是说Javascript 的对象机制并非简单的属性集合 + 原型。
Javascript 中的对象分类
宿主对象(host Objects)由宿主环境提供的对象
内置对象(Built-in Object)Javascript语言提供的对象
普通对象我们暂时不管,我们来看看剩下的几个对象。
宿主对象
先说浏览器这个宿主。
在浏览器环境中,全局对象是window,window又有很多的属性,比如document
而实际上,window上的属性,一部分当然来自浏览器环境,还有一部分来自Javascript 语言,这是由于Javascript 标准和W3C的各种标准。
与内置对象类似,宿主对象也包括固有的对象和用户创建的两种,比如document.createElement 就能创建一些DOM对象
宿主也会提供一些构造器,比如用new Image 来创建img 元素等。
内置对象-固有对象
前面提到,固有对象是标准规定的,随着Javascript 运行时创建而自动创建的对象实例。
固有对象在任何JS代码执行之前就已经创建出来了,通常扮演的是基础库的角色。前面提到的“类”就是固有对象的一种。ECMA中有150+个固有对象。但这个表格也并不完整,一会儿我们会证明。
内置对象-原生对象
JS中,能够通过语言本身的构造器创建的对象叫做原生对象。JS标准中,提供了30多个构造器,大概分为以下几类:
正如前面说的,几乎所有这些构造器的能力,是无法用纯javascript代码实现的,也无法用class/extend 语法来继承。
这些构造器创建的对象,多数使用了私有字段,如:
Error: [[ErrorData]]
Boolean: [[BooleanData]]
Number: [[NumberData]]
Date: [[DateValue]]
RegExp: [[RegExpMatcher]]
Symbol: [[SymbolData]]
Map: [[MapData]]
而这些私有字段使得原型继承方法无法正常工作,因此可以将这些原生对象看作是“特权对象”,满足某些特定的能力或者性能。
函数对象与构造器对象
我们换一个角度来看待对象,也就是,用对象来模拟函数和构造器。
JavaScript为这一类对象:
预留了私有字段机制
规定了函数对象
规定了构造器对象
函数对象:具有[[call]]私有字段的对象
构造器对象:具有[[construct]]私有字段的对象
JS用“对象模拟函数”的设计替代了一般变成语言中的函数,任何宿主只要提供了”具有[[call]]私有字段的对象“就能够被JS函数调用语法支持。(这其中其实还有些复杂,[[call]]私有字段必须是一个引擎中定义的函数,需要接受 this 值和调用参数,并且会产生域的切换,后面详细说。)
简单来说:
任何对象只要实现了[[call]], 它就是一个函数对象,可以去作为函数被调用。
任何对象如果实现了[[construct]], 它就是一个构造器对象,可以作为构造器被调用。
用户用function关键字创建的函数,必定同时是函数和构造器,不过表现出的行为与宿主及内置对象来说并不相同。
对于前面提到的宿主对象和内置对象来说,它们实现[[call]]作为函数调用和[[construct]]作为构造器调用有些不同
如Date
如浏览器的Image
再如基本类型(String、Number、Boolean),构造器当作函数调用,产生类型转换。
此外,注意ES6引入的箭头语法,创建的仅仅是函数,无法作为构造器使用。
用户使用 function 语法或者 Function 构造器创建的对象来说,[[call]]和[[construct]]行为总是相似的,它们执行同一段代码。
[[construct]] 执行过程大致如下:
以 Object.prototype 为原型创建一个新对象;
以新对象为 this,执行函数的[[call]];
如果[[call]]的返回值是对象,那么,返回这个对象,否则返回第一步创建的新对象。
而这样就会导致,如果我们的构造器返回了一个新对象,那么new创建的新对象就变成了一个构造函数之外,完全无法访问的对象,而这一定程度上实现了私有。
被特别照顾的对象
有一些对象的行为跟正常对象有很大的区别,常见的下标运算或设置原型,是与普通对象不同的,这里举几个例子:
Array:Array 的 length 属性根据最大的下标自动发生变化。
Object.prototype:作为所有正常对象的默认原型,不能再给它设置原型了。
String:为了支持下标运算,String 的正整数属性访问会去字符串里查找。
Arguments:arguments 的非负整数型下标属性跟对应的变量联动。
模块的 namespace 对象:特殊的地方非常多,跟一般对象完全不一样,尽量只用于 import 吧。
类型数组和数组缓冲区:跟内存块相关联,下标运算比较特殊。
bind 后的 function:跟原来的函数相关联。
附:JS有多少对象?
版权声明: 本文为 InfoQ 作者【张理查rootv】的原创文章。
原文链接:【http://xie.infoq.cn/article/e5e27cd572d335755941d4a7d】。文章转载请联系作者。
评论