Javacript中的对象是除了undfined数据类型以外所有其他内置类型的基础。其他所有内置类型(包括函数)的源头都可以追溯到对象。如何进行追溯呢?就是使用Object.prototype.isPrototypeOf(O)来判断O的原型链中是否包含对象原型Object.prototype,原型和原型链在后两节说明。注意这里的O必须是显式声明的对象,即利用某种类型的声明直接量或者构造函数来声明的。如下面代码段的第一第二种声明方式。这样他们的原型链上才默认有Object.prototype。声明对象的方式有三,如下。
//第一种声明对象的方式:对象直接量
let objectCreatedInWayOne = {
property1: '1',
'property 2': '2' //这里如果key中有连接符或者空格,可以用字符串表示,但是从代码风格来说不推荐
};
let array1 = [];
//第二种声明方式:使用关键字new
let objectCreatedInWayTwo = new Object({
propertyOne: '1',
propertyTwo: '2'
});
let array2 = new Aarry();
//第三种声明方式:使用create()方法 第一个参数为原型
let objectCreatedInWayThree = Object.create(Object.prototype, {
propertyOne: '1',
propertyTwo: '2'
});
let array3 = Object.create(Array.prototype, []);
对象的属性分为两种类型:数值属性和存取器属性(ES5中的特性)。数值属性是我们平常用的比较多的,直接给对象定义key: value。而存取器属性则比较复杂,一个存取器属性可以有一对getter和setter函数组成,也可以仅由一个getter函数组成,这样前者是一个读/写属性,而后者是一个读属性。对于存取器属性的定义以及读写,如下面代码所示。在存取器函数中之所以能用this关键字获取和改变对象的其他属性,是因为读写size时候相当于该对象调用了size的get\set方法,会向方法内传入一个默认的实参,为该对象,使用this关键就可以获取该实参。(这点可以参考Function类型的定义与调用)。
//定义属性
let objectForDisplayProperties = {
width: 10, //数值属性
length: 10, //数值属性
get size() { //存取器属性的getter
return this.width * this.height;
},
set size(size) { //存取器属性的setter
this.length = size / this.width;
}//定义好后,size就是一个可读可写的属性。
}
//读写属性,属性访问表达式有两种一种是 . 一种是 []
objectForDisplayProperties.size === 100;//true
objectForDisplayProperties['size'] = 50;
objectForDisplayProperties.length === 5;//true
对象的属性的读写和配置则是由对象的可拓展性以及各个属性的特性来决定的。其中不同类型的属性的特性描述符不一样。下文代码段中展示了获取width属性以及size的特性描述符。可以看到数值属性与存取器属性都有enumerable和configurable属性。其中描述符对象中的configurable代表该属性是否可以被配置,如果设为true的话,则该属性的整个特性描述符对象都可以被更改,甚至从数值属性变成存取器属性,反之亦可。描述符对象中的enumerable则代表该属性是否是可以枚举的。是否可以枚举主要用于对象的各种方法调用中,对属性进行区分。具体如何区分见下面代码段。一般不可枚举属性都是继承自原型链上的属性。总结一下:可枚举这个描述符中属性的一大作用,就是把对象原型链上继承的方法属性和对象自身定义的属性区分开。
//说明为什么我通篇都用单引号表示字符串呢,是因为统一的代码规范如此定义。
//获取数值属性width的特性描述符
Object.getOwnPropertyDescriptor(objectForDisplayProperties, 'width');
输出结果:
{
value: 10, //值
writable: true, //是否可写入,更改值value
enumerable: true, //是否可枚举
configurable: true //是否可配置
}
Object.getOwnPropertyDescriptor(objectForDisplayProperties, 'size');
输出结果:
{
get: f (), //getter函数
set: f (size), //setter函数
enumerable: true, //是否可枚举
configurable: true //是否可配置
}
//是否可枚举的影响。
for (property in objectForDisplayProperties) {
//会遍历对象中所有的可枚举属性,包括原型链上的。
if (objectForDisplayProperties.hasOwnProperty(property)) {
//过滤对象自身拥有的中的属性
}
}
objectForDisplayProperties.forEach(() => {
//只会遍历对象自身拥有的可枚举属性
});
JSON.stringify(objectForDisplayProperties);//只会序列化对象自身拥有的中的可枚举属性
//还有很多对象特定的内置方法都只对对象自身有用的中的可枚举属性起作用。
//设置对象属性
let objectTesting = {pro: 'test'};
Object.defineProperty(objectTesting , 'pro', {
writable: false,
configurable: false
});
objectTesting.pro= 'try modifying';
//抛出异常,因为该值已经不能修改。
Object.defineProperty(objectTesting , 'pro', {writable: true});
//抛出异常,因为该属性已经不能再进行配置。
//设置对象为不可拓展,该过程不可逆。
Object.preventExtensions(objectTesting);
objectTesting.newPro = 'try adding new pro';//抛出异常,不可拓展对象不能新增属性
Object.isSealed(objectTesting);//true
//sealed状态其实就是对象的所有属性都不可配置且该对象不可拓展,所以为true。可以通过Object.seal(object)直接设置。
Object.isFrozen(objectTesting);//true
//frozen状态其实就是对象的所有属性都不可更改、不可配置且该对象不可拓展,所以为true。可以通过Object.freeze(object)直接设置。
讲完对象的属性分类,还需要讲一下对象的三个内置的属性:与之相关联的原型(prototype)、类名(class)以及可拓展性(extensible attribute);可拓展性表示该对象是否可以可以新增属性,操作如上述代码段末尾所示。类名则表示该对象在javascript中系统给它的分类。最后则是原型属性,原型属性也是我们接下来要重点讲的。
每一个Javascript对象都和另外一个对象关联。“另外一个”对象就是我们熟知的原型。每一个对象都从原型继承属性。继承来的属性就放在该对象内置的原型属性的value中。大部分情况下对象都从原型继承的属性都是方法,ES6中的extend语法糖的实现估计就是应用了原型继承。所有通过对象直接量创建的对象都拥有同一个原型,Object.prototype。通过关键字new和调用构造函数所创建的对象的原型,就是构造函数的prototype属性的值,如下代码段所示。注意一般不能通过访问属性prototype来直接访问某个对象的原型,建议用Object.getPrototypeOf()。实测:在Chorme中,对象的原型存放在__proto__属性中。
let originalObject = {};
let createdObject = Object.create(originalObject, {});
//Object.getPrototypeOf(object) 获取对象object的原型
Object.getPrototypeOf(originalObject) === Object.prototype;//true
Object.getPrototypeOf(originalObject) === Object.prototype;//false,该对象原型为originalObject, 是空对象
let array = new Array();
let symboledArray = [];
Object.getPrototypeOf(array) === Array.prototype;//true
Object.getPrototypeOf(symboledArray) === Array.prototype;//true
在文章开头就已经说过对象是除了undfined数据类型以外所有其他内置类型的基础,也就是说许多内置类型的原型链上都包含Object.prototype。
那么什么是原型链呢?拿数组类型来说吧。数组类型的构造函数中定义了数组类型自身应该包括什么原型属性方法(一般都是方法,比如concat: function (), slice: function ()等),然后由于数组类型在定义上是继承自Object类型的,因此在定义在数组类型的构造函数的原型中还会有一个特殊属性,存储的是父级的原型。这样层层嵌套就形成了一条原型的链。在每种数据类型的的构造函数的原型中都包含了一条抵达最上级父级的原型的链路。当数组类型初始化时,数组实例就会从原型中继承属性,放到数组实例的原型属性(Chrome中为__proto__)中,数组实例也就拥有了一条原型链。
let arrayTest= [];
Object.getPrototypeOf(Object.getPrototypeOf(arrayTest)) === Object.prototype;//true
//从上述式子可以知道数组的原型链的层级有两层,经过两层溯源就到对象原型了
原型链好像挺厉害的,那有什么用的?一大用处就是用来进行面向对象编程,定义一类对象的通用行为(方法)。
还是拿数组来说事,数组实例对象的属性中其实是没有直接定义concat: function(), slice: function()等对数组进行操作的方法的。这些方法都放在数组的原型属性中,当你对数组访问了concat属性,解析器首先会去数组的属性中去查找key为concat的属性,找不到后再进入原型链中从底层逐层向上查找,直到找到或者抛出异常。很幸运,我们数组在原型链的最底层就找到了concat属性,调用了concat方法。那要是我们访问的是toString属性呢?你会发现数组的原型中没有这个属性,只好耐住寂寞往原型链上端爬,爬到Object这一层的原型,才发现了toString属性,调用了toString方法,于是很开心地把自己变成了一个以逗号隔开的字符串。讲到这里,你可能已经发现了,原型链可以让我们在javascript中实现面向对象编程中的继承与多态。