共计 10633 个字符,预计需要花费 27 分钟才能阅读完成。
JavaScript 中,除了五种原始类型(即数字,字符串,布尔值,null,undefined)之外的都是对象了,所以,不把对象学明确怎么持续往下学习呢?
一. 概述
对象是一种复合值,它将很多值(原始值或其余对象)聚合在一起,可通过属性名拜访这些值。而属性名能够是蕴含空字符串在内的任意字符串。JavaScript 对象也能够称作一种数据结构,正如咱们常常据说的“散列(hash)”、“散列表(hashtable)”、“字典(dictionary)”、“关联数组(associative array)”。
JavaScript 中对象能够分为三类:
①内置对象,例如数组、函数、日期等;
②宿主对象,即 JavaScript 解释器所嵌入的宿主环境(比方浏览器)定义的,例如 HTMLElement 等;
③自定义对象,即程序员用代码定义的;
对象的属性能够分为两类:
①自有属性(own property):间接在对象中定义的属性;
②继承属性(inherited property):在对象的原型对象中定义的属性(对于原型对象上面会详谈);
二. 对象的创立
既然学习对象,又怎能不懂如何创建对象呢?面试前端岗位的同学,可能都被问过这个根底问题吧:
创立 JavaScript 对象的两种办法是什么?(或者:说说创立 JavaScript 对象的办法?)
这个问题我就被问过两次。“创建对象的两种办法”这种说法网上有很多,然而据我所看书籍来说是有三种办法的!上面咱们就来具体谈谈这三种办法:
1.对象间接量
对象间接量由若干名 / 值对组成的映射表,名 / 值对两头用冒号分隔,名 / 值对之间用逗号分隔,整个映射表用花括号括起来。属性名能够是 JavaScript 标识符也能够是字符串间接量,也就是说上面两种创建对象 obj 的写法是齐全一样的:
var obj = {x: 1, y: 2};
var obj = {‘x’: 1, ‘y’:2};
2.通过 new 创建对象
new 运算符后追随一个函数调用,即构造函数,创立并初始化一个新对象。例如:
1 var o = new Object(); // 创立一个空对象,和 {} 一样
2 var a = new Array(); // 创立一个空数组,和 [] 一样
3 var d = new Date(); // 创立一个示意以后工夫的 Date 对象
对于构造函数相干的内容当前再说。
3.Object.create()
ECMAScript5 定义了一个名为 Object.create()的办法,它创立一个新对象,其中第一个参数是这个对象的原型对象(如同还没解释原型对象 … 上面马上就说),第二个可选参数用以对对象的属性进行进一步的形容,第二个参数上面再说(因为这第三种办法是 ECMAScript5 中定义的,所以以前大家才常常说创建对象的两种办法的吧?集体感觉应该是这个起因)。这个办法应用很简略:
1 var o1 = Object.create({x: 1, y: 2}); // 对象 o1 继承了属性 x 和 y
2 var o2 = Object.create(null); // 对象 o2 没有原型
上面三种的齐全一样的:
1 var obj1 = {};
2 var obj2 = new Object();
3 var obj3 = Object.create(Object.prototype);
为了解释为啥这三种形式是齐全一样的,咱们先来解释下 JavaScript 中的原型对象(哎,让客官久等了!),记得一位大神说过:
Javascript 是一种基于对象(object-based)的语言,你遇到的所有货色简直都是对象。然而,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有 class(类)。
面向对象的编程语言 JavaScript,没有类!!!那么,它是怎么实现继承的呢?没错,就是通过原型对象。基本上每一个 JavaScript 对象(null 除外)都和另一个对象相关联,“另一个”对象就是所谓的原型对象(原型对象也能够简称为原型,并没有设想的那么简单,它也只是一个对象而已)。每一个对象都从原型对象继承属性,并且一个对象的 prototype 属性的值(这个属性在对象创立时默认主动生成,并不需要显示的自定义)就是这个对象的原型对象,即 obj.prototype 就是对象 obj 的原型对象。
原型对象先说到这,回到下面的问题,有了对原型对象的意识,上面就是不须要过多解释的 JavaScript 语言规定了:
①所有通过对象间接量创立的对象的原型对象就是 Object.prototype 对象;
②通过关键字 new 和构造函数创立的对象的原型对象就是构造函数 prototype 属性的值,所以通过构造函数 Object 创立的对象的原型就是 Object.prototype 了;
当初也补充了第三种创建对象的办法 Object.create()第一个参数的含意。
三. 属性的查问和设置
学会了如何创建对象还不够啊,因为对象只有领有一些属性能力真正起到作用滴!那么,就持续往下学习对象的属性吧!
能够通过点(.)或方括号([])运算符来获取和设置属性的值。对于点(.)来说,右侧必须是一个以属性名命名的标识符(留神:JavaScript 语言的标识符有本人的非法规定,并不同于带引号的字符串);对于方括号([])来说,方括号内必须是一个字符串表达式(字符串变量当然也能够喽,其余能够转换成字符串的值比方数字什么的也是都能够滴),这个字符串就是属性的名字。正如上面例子:
1 var obj = {x: 1, y: 2};
2 obj.x = 5;
3 obj[‘y’] = 6
概述中说过,JavaScript 对象具备”自有属性“,也有“继承属性”。当查问对象 obj 的属性 x 时,首先会查找对象 obj 自有属性中是否有 x,如果没有,就会查找对象 obj 的原型对象 obj.prototype 是否有属性 x,如果没有,就会进而查找对象 obj.prototype 的原型对象 obj.prototype.prototype 是否有属性 x,就这样直到找到 x 或者查找到的原型对象是 undefined 的对象为止。能够看到,一个对象下面继承了很多原型对象,这些原型对象就形成了一个”链“,这也就是咱们平时所说的“原型链”,这种继承也就是 JavaScript 中“原型式继承”(prototypal inheritance)。
对象 o 查问某一属性时正如下面所说会沿着原型链一步步查找,然而其设置某一属性的值时,只会批改自有属性(如果对象没有这个属性,那就会增加这个属性并赋值),并不会批改原型链上其余对象的属性。
四. 存取器属性 getter 和 setter
下面咱们所说的都是很一般的对象属性,这种属性称做“数据属性”(data property),数据属性只有一个简略的值。然而在 ECMAScript 5 中,属性值能够用一个或两个办法代替,这两个办法就是 getter 和 setter,有 getter 和 setter 定义的属性称做“存取器属性”(accessor property)。
当程序查问存取器属性的值时,JavaScript 调用 getter 办法(无参数)。这个办法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript 调用 setter 办法,将赋值表达式右侧的值当做参数传入 setter。如果属性同时具备 getter 和 setter 办法,那么它就是一个读 / 写属性;如果它只有 getter 办法,那么它就是一个只读属性,给只读属性赋值不会报错,然而并不能胜利;如果它只有 setter 办法,那么它是一个只写属性,读取只写属性总是返回 undefined。看个理论的例子:
1 var p = {
2 x: 1.0,
3 y: 2.0,
4 get r(){ return Math.sqrt(this.xthis.x + this.ythis.y); };
5 set r(newvalue){
6 var oldvalue = Math.sqrt(this.xthis.x + this.ythis.y);
7 var ratio = newvalue/oldvalue;
8 this.x *= ratio;
9 this.y *= ratio;
10 },
11 get theta(){ return Math.atan2(this.y, this.x); },
print: function(){ console.log(‘x:’+this.x+’, y:’+this.y); }
12 };
正如例子所写,存取器属性定义一个或两个和属性同名的函数,这个函数定义并没有应用 function 关键字,而是应用 get 和 set,也没有应用冒号将属性名和函数体分隔开。比照一下,上面的 print 属性是一个函数办法。留神:这里的 getter 和 setter 里 this 关键字的用法,JavaScript 把这些函数当做对象的办法来调用,也就是说,在函数体内的 this 指向这个对象。上面看下实例运行后果:
正如控制台的输入,r、theta 同 x,y 一样只是一个值属性,print 是一个办法属性。
ECMAScript 5 减少的这种存取器,尽管比一般属性更为简单了,然而也使得操作对象属性键值对更加谨严了。
五. 删除属性
程序猿撸码个别都是实现增、删、改、查性能,后面曾经说了增、改、查,上面就说说删除吧!
delete 运算符能够删除对象的属性,它的操作数应该是一个属性拜访表达式。然而,delete 只是断开属性和宿主对象的分割,而不会去操作属性中的属性:
1 var a = {p:{x:1}};
2 var b = a.p;
3 delete a.p;
执行这段代码后 b.x 的值仍然是 1,因为已删除属性的援用仍然存在,所以有时这种不谨严的代码会造成内存泄露,所以在销毁对象的时候,要遍历属性中的属性,顺次删除。
delete 表达式返回 true 的状况:
①删除胜利或没有任何副作用(比方删除不存在的属性)时;
②如果 delete 后不是一个属性拜访表达式;
1 var obj = {x: 1,get r(){return 5;},set r(newvalue){this.x = newvalue;}};
2 delete obj.x; // 删除对象 obj 的属性 x,返回 true
3 delete obj.x; // 删除不存在的属性,返回 true
4 delete obj.r; // 删除对象 obj 的属性 r,返回 true
5 delete obj.toString; // 没有任何副作用(toString 是继承来的,并不能删除),返回 true
6 delete 1; // 数字 1 不是属性拜访表达式,返回 true
delete 表达式返回 false 的状况:
①删除可配置性(可配置性是属性的一种个性,上面谈判到)为 false 的属性时;
1 delete Object.prototype; // 返回 false,prototype 属性是不可配置的
2 // 通过 var 申明的变量或 function 申明的函数是全局对象的不可配置属性
3 var x = 1;
4 delete this.x; // 返回 false
5 function f() {}
6 delete this.f; // 返回 false
六. 属性的个性
下面曾经说到了属性的可配置性个性,因为上面要说的检测属性和枚举属性还要用到属性的个性这些概念,所以当初就先具体说说属性的个性吧!
除了蕴含名字和值之外,属性还蕴含一些标识它们可写、可枚举、可配置的三种个性。在 ECMAScript 3 中无奈设置这些个性,所有通过 ECMAScript 3 的程序创立的属性都是可写的、可枚举的和可配置的,且无奈对这些个性做批改。ECMAScript 5 中提供了查问和设置这些属性个性的 API。这些 API 对于库的开发者十分有用,因为:
①能够通过这些 API 给原型对象增加办法,并将它们设置成不可枚举的,这让它们更像内置办法;
②能够通过这些 API 给对象定义不能批改或删除的属性,借此“锁定”这个对象;
在这里咱们将存取器属性的 getter 和 setter 办法看成是属性的个性。依照这个逻辑,咱们也能够把属性的值同样看做属性的个性。因而,能够认为属性蕴含一个名字和 4 个个性。数据属性的 4 个个性别离是它的值(value)、可写性(writable)、可枚举性(enumerable)和可配置性(configurable)。存取器属性不具备值个性和可写性它们的可写性是由 setter 办法是否存在与否决定的。因而存取器属性的 4 个个性是读取(get)、写入(set)、可枚举性和可配置性。
为了实现属性个性的查问和设置操作,ECMAScript 5 中定义了一个名为“属性描述符”(property descriptor)的对象,这个对象代表那 4 个个性。描述符对象的属性和它们所形容的属性个性是同名的。因而,数据属性的描述符对象的属性有 value、writable、enumerable 和 configurable。存取器属性的描述符对象则用 get 属性和 set 属性代替 value 和 writable。其中 writable、enumerable 和 configurable 都是布尔值,当然,get 属性和 set 属性是函数值。通过调用 Object.getOwnPropertyDescriptor()能够取得某个对象特定属性的属性描述符:
从函数名字就能够看出,Object.getOwnPropertyDescriptor()只能失去自有属性的描述符,对于继承属性和不存在的属性它都返回 undefined。要想取得继承属性的个性,须要遍历原型链(不会遍历原型链?不要急,上面会说到的)。
要想设置属性的个性,或者想让新建属性具备某种个性,则须要调用 Object.definePeoperty(),传入须要批改的对象、要创立或批改的属性的名称以及属性描述符对象:
能够看到:
①传入 Object.defineProperty()的属性描述符对象不用蕴含所有 4 个个性;
②可写性管制着对属性值的批改;
③可枚举性管制着属性是否可枚举(枚举属性,上面会说的);
④可配置性管制着对其余个性(包含后面说过的属性是否能够删除)的批改;
如果要同时批改或创立多个属性,则须要应用 Object.defineProperties()。第一个参数是要批改的对象,第二个参数是一个映射表,它蕴含要新建或批改的属性的名称,以及它们的属性描述符,例如:
var p = Object.defineProperties({},{
x: {value: 1, writable: true, enumerable: true, configurable: true},
y: {value: 2, writable: true, enumerable: true, configurable: true},
r: {get: function(){return 88;}, set: function(newvalue){this.x =newvalue;},enumerable: true, configurable: true},
greet: {value: function(){console.log(‘hello,world’);}, writable: true, enumerable: true, configurable: true}
});
置信你也曾经从实例中看出:Object.defineProperty()和 Object.defineProperties()都返回批改后的对象。
后面咱们说 getter 和 setter 存取器属性时应用对象间接量语法给新对象定义存取器属性,但并不能查问属性的 getter 和 setter 办法或给已有的对象增加新的存取器属性。在 ECMAScript 5 中,就能够通过 Object.getOwnPropertyDescriptor()和 Object.defineProperty()来实现这些工作啦!但在 ECMAScript 5 之前,大多数浏览器(IE 除外啦)曾经反对对象间接量语法中的 get 和 set 写法了。所以这些浏览器还提供了非标准的老式 API 用来查问和设置 getter 和 setter。这些 API 有 4 个办法组成,所有对象都领有这些办法。__lookupGetter__()和__lookupSetter__()用以返回一个命名属性的 getter 和 setter 办法。__defineGetter__()和__defineSetter__()用以定义 getter 和 setter。这四个办法都是以两条下划线做前缀,两条下划线做后缀,以表明它们是非规范办法。
七. 检测属性
JavaScript 对象能够看做属性的汇合,那么咱们有时就须要判断某个属性是否存在于某个对象中,这就是接下来要说的检测属性。
检测一个对象的属性也有三种办法,上面就来具体说说它们的作用及区别!
1.in运算符
in 运算符左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中蕴含这个属性则返回 true,否则返回 false。
为了试验,咱们先给对象 Object.prototype 增加一个可枚举属性 m,一个不可枚举属性 n;而后,给对象 obj 定义两个源码交易可枚举属性 x, 一个不可枚举属性 y,并且对象 obj 是通过对象间接量模式创立的,继承了 Object.prototype。
从运行后果能够看出:in 运算符左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性(不管这些属性是否可枚举)中蕴含这个属性则返回 true,否则返回 false。
2.hasOwnProperty()
对象的 hasOwnProperty()办法用来检测给定的名字是否是对象的自有属性(不管这些属性是否可枚举),对于继承属性它将返回 false。
3.propertyIsEnumerable()
propertyIsEnumerable()是 hasOwnProperty()的增强版,只有检测到是自有属性且这个属性可枚举性为 true 时它才返回 true。
八. 枚举属性
绝对于检测属性,咱们更罕用的是枚举属性。枚举属性咱们通常应用 for/in 循环,它能够在循环体中遍历对象中所有可枚举的自有属性和继承属性,把属性名称赋值给循环变量。
我原来认为 for/in 循环跟 in 运算符有莫大关系的,当初看来它们的规定并不相同啊!当然,如果这里不想遍历出继承的属性,那就在 for/in 循环中加一层 hasOwnProperty()判断:
for(prop in obj){
if(obj.hasOwnProperty(prop)){
console.log(prop);
}
}
除了 for/in 循环之外,ECMAScript 5 还定义了两个能够枚举属性名称的函数:
①Object.getOwnpropertyNames(),它返回对象的所有自有属性的名称,不管是否可枚举;
②Object.keys(),它返回对象对象中可枚举的自有属性的名称;
九. 对象的三个非凡属性
每个对象都有与之相干的原型(prototype)、类(class)和可扩展性(extensible attribute)。这三个就是对象的非凡属性(它们也只是对象的属性而已,并没有设想的简单哦)。
1.原型属性
正如后面所说,对象的原型属性是用来继承属性的(有点绕 …),这个属性如此重要,以至于咱们常常把“o 的原型属性”间接叫做“o 的原型”。原型属性是在实例创立之初就设置好的(也就是说,这个属性的值是 JavaScript 默认主动设置的,前面咱们会说如何本人手动设置),后面也提到:
①通过对象间接量创立的对象应用 Object.prototype 作为它们的原型;
②通过 new+ 构造函数创立的对象应用构造函数的 prototype 属性作为它们的原型;
③通过 Object.create()创立的对象应用第一个参数(如果这个参数为 null,则对象原型属性值为 undefined;如果这个参数为 undefined,则会报错:Uncaught TypeError: Object prototype may only be an Object or null: undefined)作为它们的原型;
那么,如何查问一个对象的原型属性呢?在 ECMAScript 5 中,将对象作为参数传入 Object.getPrototypeOf()能够查问它的原型,例如:
然而在 ECMAScript 3 中,没有 Object.getPrototypeOf()函数,但常常应用表达式 obj.constructor.prototype 来检测一个对象的原型,因为每个对象都有一个 constructor 属性示意这个对象的构造函数:
①通过对象间接量创立的对象的 constructor 属性指向构造函数 Object();
②通过 new+ 构造函数创立的对象的 constructor 属性指向构造函数;
③通过 Object.create()创立的对象的 constructor 属性指向与其原型对象的 constructor 属性指向雷同;
要检测一个对象是否是另一个对象的原型(或处于原型链中),能够应用 isPrototypeOf()办法。例如:
还有一个非标准但泛滥浏览器都已实现的对象的属性__proto__(同样是两个下划线开始和完结,以表明其为非标准),用以间接查问 / 设置对象的原型。
2.类属性
对象的类属性(class attribute)是一个字符串,用以示意对象的类型信息。ECMAScript 3 和 ECMAScript 5 都未提供设置这个属性的办法,并只有一种间接的办法能够查问它。默认的 toString()办法(继承自 Object.prototype)返回了这种格局的字符串:[object class]。因而,要想取得对象的类,能够调用对象的 toString()办法,而后提取已返回字符串的第 8 到倒数第二个地位之间的字符。不过,很多对象继承的 toString()办法重写了(比方:Array、Date 等),为了能调用正确的 toString()版本,必须间接地调用 Function.call()办法。上面代码能够返回传递给它的任意对象的类:
1 function classof(obj){
2 if(o === null){
3 return ‘Null’;
4 }
5 if(o === undefined){
6 return ‘Undefined’;
7 }
8 return Object.prototype.toString.call(o).slice(8, -1);
9 }
classof()函数能够传入任何类型的参数。上面是应用实例:
总结:从运行后果能够看出通过三种形式创立的对象的类属性都是 ’Object’。
3.可扩展性
对象的可扩展性用以示意是否能够给对象增加新属性。所有内置对象和自定义对象都是显示可扩大的(除非将它们转换为不可扩大),宿主对象的可扩展性是由 JavaScript 引擎定义的。ECMAScript 5 中定义了用来查问和设置对象可扩展性的函数:
①(查问)通过将对象传入 Object.isExtensible(),来判断该对象是否是可扩大的。
②(设置)如果想将对象转换为不可扩大,须要调用 Object.preventExtensions(),将待转换的对象作为参数传进去。留神:
a. 一旦将对象转换为不可扩大的,就无奈再将其转换回可扩大的了;
b.preventExtensions()只影响到对象自身的可扩展性,如果给一个不可扩大的对象的原型增加属性,这个不可扩大的对象同样会继承这些新属性;
进一步,Object.seal()和 Object.preventExtensions()相似,除了能将对象设置为不可扩大的,还能够将对象的所有自有属性都设置为不可配置的。对于那些曾经关闭(sealed)起来的对象是不能解封的。能够应用 Object.isSealed()来检测对象是否关闭。
更进一步,Object.freeze()将更严格地锁定对象——“解冻”(frozen)。除了将对象设置为不可扩大和将其属性设置为不可配置之外,还能够将它自有的所有数据属性设置为只读(若对象的存取器属性有 setter 办法,存取器属性将不受影响,仍可通过给属性赋值调用它们)。应用 Object.isFrozen()来检测对象是否总结。
总结:Object.preventExtensions()、Object.seal()和 Object.freeze()都返回传入的对象,也就是说,能够通过嵌套的形式调用它们:
1 var obj = Object.seal(Object.create(Object.freeze({x:1}),{y:{value: 2, writable: true}));
这条语句中应用 Object.create()函数传入了两个参数,即第一个参数是创立出的对象的原型对象,第二个参数是在创建对象是间接给其定义的属性,并且附带定义了属性的个性。
十. 对象的序列化
后面说完了对象的属性以及对象属性的个性,货色还是蛮多的,不晓得你是否已看晕。不过,上面就是比拟轻松的话题了!
对象序列化(serialization)是指将对象的状态转换为字符串,也能够将字符串还原为对象。ECMAScript 5 提供了内置函数 JSON.stringify()和 JSON.parse()用来序列化和还原对象。这些办法都应用 JSON 作为数据交换格局,JSON 的全称是“JavaScript Object Notation”——JavaScript 对象表示法,它的语法和 JavaScript 对象与数组间接量的语法十分相近:
其中,最初的 jsonObj 是 obj 的深拷贝
JSON 的语法是 JavaScript 的子集,它并不能示意 JavaScript 里的所有值。反对对象、数组、字符串、无穷大数字、true、false 和 null,并且它们能够序列化和还原。留神:
①NaN、Infinity 和 -Infinity 序列化的后果是 null;
②JSON.stringify()只能序列化对象可枚举的自有属性;
③日期对象序列化的后果是 ISO 格局的日期字符串(参照 Date.toJSON()函数),但 JSON.parse()仍然保留它们的字符串状态,而不能将它们还原为原始日期对象;
④函数、RegExp、Error 对象和 undefined 值不能序列化和还原;
当然,JSON.stringify()和 JSON.parse()都能够承受第二个可选参数,通过传入须要序列化或还原的属性列表来定制自定义的序列化或还原操作,这个咱们当前再详谈。