对象能够经过两种形式定义:声明(文字)形式和构造形式。算法
对象的文字语法大概是这样:数组
var myObj = {
key: value,
// ...
};
复制代码
构造形式大概是这样:安全
var myObj = new Object();
myObj.key = value;
复制代码
构造形式和文字形式生成的对象是同样的。惟一的区别是,在文字声明中你能够添加多个键值对,可是在构造形式中你必须逐个添加属性。数据结构
对象是JavaScript的基础。在JavaScript中一共有六种主要类型(术语是“语言类型”):函数
注意,简单基本类型(string,boolean,number,null和undefined)自己并非对象。 null有时会被看成一种对象类型,可是这其实只是语言自己的一个bug,即对null执行typeof null时会返回字符串"object"。实际上,null自己是基本类型。学习
JavaScript中有许多特殊的对象子类型,咱们能够称之为复杂基本类型。优化
函数就是对象的一个子类型(从技术角度来讲就是“可调用的对象”)。JavaScript中的函数是“一等公民”,由于它们本质上和普通的对象同样(只是能够调用),因此能够像操做其余对象同样操做函数(好比看成另外一个函数的参数)。ui
数组也是对象的一种类型,具有一些额外的行为。数组中内容的组织方式比通常的对象要稍微复杂一些。this
JavaScript中还有一些对象子类型,一般被称为内置对象。有些内置对象的名字看起来和简单基础类型同样,不过实际上它们的关系更复杂。spa
这些内置对象实际上只是一些内置函数。这些内置函数能够看成构造函数(由new产生的函数调用)来使用,从而能够构造一个对应子类型的新对象。
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String("I am a string");
typeof strObject; // "object"
strObject instanceof String; // true
// 检查sub-type对象
Object.prototype.toString.call(strObject); // [Object String]
复制代码
原始值“I am a string”并非一个对象,它只是一个字面量,而且是一个不可变的值。若是要在这个字面量上执行一些操做,好比获取长度、访问其中某个字符等,那须要将其转换为String对象。
幸亏,在必要时语言会自动把字符串字面量转换成一个String对象,也就是说你并不须要显式建立一个对象。JavaScript社区中的大多数人都认为能使用文字形式时就不要使用构造形式。
var strPrimitive = "I am a string";
console.log(strPrimitive.length); // 13
console.log(strPrimitive.charAt(3)); // "m"
复制代码
以上两种方法,均可以直接在字符串字面量上访问属性或者方法,之因此能够这样,是由于引擎自动把字面量转换成String对象,因此能够访问属性和方法。
一样的事也会发生在数值字面量上,若是使用相似42.359.toFixed(2)的方法,引擎会把42转换成new Number(42)。对于布尔字面量来讲也是如此。
null和undefined没有对应的构造形式,它们只有文字形式。相反,Date只有构造,没有文字形式。
对于Object、Array、Function和RegExp来讲,不管使用文字形式仍是构造形式,它们都是对象,不是字面量。
Error对象不多在代码中显式建立,通常是在抛出异常时被自动建立。
对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,咱们称之为属性。
var myObject = {
a: 2
}
myObject.a; // 2
myObject['a']; // 2
复制代码
.a语法一般被称为“属性访问”,["a"]语法一般被称为“键访问”。实际上它们访问的是同一个位置,而且会返回相同的值2,而且这两个术语是能够互换的。
这两种语法的主要区别在于.操做符要求属性名知足标识符的命名规范,而[""]语法能够接受任意UTF-8/Unicode字符串做为属性名。
var myObject = {
a: 2
}
var idx;
if(wantA) {
idx = "a";
}
console.log(myObject[idx]); // 2
复制代码
在对象中,属性名永远都是字符串。若是你使用string(字面量)之外的其余值做为属性名,那它首先会被转换为一个字符串。即便是数字也不例外,虽然在数组下标中使用的的确是数字,可是在对象属性名中数字会被转换成字符串,因此小心不要搞混对象和数组中数字的用法:
var myObject = {};
myObject[true] = "foo";
myObject[3] = "bar";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
复制代码
ES6增长了可计算属性名,能够在文字形式中使用[]包裹一个表达式来看成属性名:
var prefix = "foo";
var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world"
}
myObject["foobar"]; // hello
myObject["foobaz"]; // world
复制代码
不管返回值是什么类型,每次访问对象的属性就是属性访问。若是属性访问返回的是一个函数,那它并非一个“方法”。属性访问返回的函数和其余函数没有任何区别(除了可能发生的隐式绑定this)。
function foo() {
console.log("foo");
}
var someFoo = foo; // 对foo的变量引用
var myObject = {
someFoo: foo
}
foo; // function foo() {...}
someFoo; // function foo() {...}
myObject.someFoo; // function foo() {...}
复制代码
someFoo和myObject.someFoo只是对于同一个函数的不一样引用,并不能说明这个函数是特别的或者“属于”某个对象。若是foo()定义时内部有一个this引用,那这两个函数引用的惟一区别就是myObject.someFoo中的this会被隐式绑定到一个对象。
即便你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象--它们只是对于相同函数对象的多个引用。
var myObject = {
foo: function() {
console.log("foo");
}
}
var someFoo = myObject.foo;
someFoo; // function foo() {...}
myObject.foo; // function foo() {...}
复制代码
数组也支持[]访问形式,可是数组指望的是数值下标,也就是说值存储的位置(一般被称为索引)是非负整数,好比说0和42:
var myArray = ['foo', 42, 'bar'];
myArray.length; // 3
myArray[0]; // 'foo'
myArray[2]; // 'bar'
复制代码
数组也是对象,因此虽然每一个下标都是整数,你仍然能够给数组添加属性:
var myArray = ['foo', 42, 'bar'];
myArray.baz = 'baz';
myArray.length; // 3
myArray.baz; // 'baz'
复制代码
能够看到虽然添加了命名属性(不管是经过.语法仍是[]语法),数组的length值并未发生变化。
数组和普通的对象都根据其对应的行为和用途进行了优化,因此最好只用对象来存储键值对,只用数组来存储数值下标/值对。
注意:若是你视图向数组添加一个属性,可是属性名“看起来”像一个数字,那它会变成一个数值下标(所以会修改数组的内容而不是添加一个属性):
var myArray = ['foo', 42, 'bar'];
myArray['3'] = 'baz';
myArray.length; // 4
myArray[3]; // 'baz'
复制代码
JavaScript初学者最多见的问题之一就是如何复制一个对象。
举例来讲,思考一下这个对象:
function anotherFunction() { /*...*/ }
var anotherObject = {
c: true
}
var anotherArray = [];
var myObject = {
a: 2,
b: anotherObject, // 引用,不是复本!
c: anotherArray, // 另外一个引用
d: anotherFunction
}
anotherArray.push(anotherObject, myObject);
复制代码
如何准确地表示myObject的复制呢?
首先,咱们应该判断它是浅拷贝仍是深拷贝。对于浅拷贝来讲,复制出的新对象中a的值会复制旧对象中a的值,也就是2,可是新对象中b,c,d三个属性其实只是三个引用,它们和旧对象中b,c,d引用的对象是同样的。对于深拷贝来讲,除了复制myObject之外还好复制anotherObject和anotherArray。这时问题来了,anotherArray引用了anotherObject和myObject,因此又须要复制myObject,这样就会因为循环引用致使死循环。
对于JSON安全(也就是说能够被序列化为一个JSON字符串而且能够根据这个字符串解析出一个结构和值彻底同样的对象)的对象来讲,有一种巧妙的深拷贝方法:
var newObj = JSON.parse(JSON.stringify(someObj));
复制代码
固然,这种方法须要保证JSON安全的,因此只适用于部分状况。
ES6定义了Object.assign(..)方法来实现浅拷贝。Object.assign(..)方法的第一个参数是目标对象,以后还能够跟一个或多个源对象。它会遍历一个或多个源对象的全部可枚举的自由键并把它们赋值(使用=操做符赋值)到目标对象,最后返回目标对象,就像这样:
var newObj = Object.assign({}, myObject);
newObj.a; //2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true
复制代码
在ES5以前,JavaScript语言自己并无提供能够直接检测属性特性的方法,好比判断属性是否可读。
可是从ES5开始,全部的属性都具有了属性描述符。
思考下面的代码:
var myObject = {
a: 2
}
Object.getOwnPropertyDescriptor(myObject, 'a');
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
复制代码
如你所见,这个普通的对象属性对应的属性描述符可不只仅只是一个2。它还包好另外三个特性:writable(可写)、enumerable(可枚举)、和configurable(可配置)。
在建立普通属性时属性描述符会使用默认值,咱们也可使用Object.defineProperty(..)来添加一个新属性或者修改一个已有属性(若是它是configurable)并对特性进行设置。
var myObject = {};
Object.defineProperty(myObject, 'a', {
value: 2,
writable: true,
configurable: true,
enumerable: true
})
myObject.a; // 2
复制代码
咱们使用defineProperty(..)给myObject添加了一个普通的属性并显式指定了一些特性。然而,通常来讲你不会使用这种方式,除非你想修改属性描述符。
writable决定是否能够修改属性的值。
var myObject = {};
Object.defineProperty(myObject, 'a', {
value: 2,
writable: false, // 不可写
configurable: true,
enumerable: true
})
myObject.a = 3;
myObjec.a; // 2
复制代码
若是在严格模式下,这种方法会出错:
'use strict'
var myObject = {};
Object.defineProperty(myObject, 'a', {
value: 2,
writable: false, // 不可写
configurable: true,
enumerable: true
})
myObject.a = 3; // TypeError
复制代码
只要属性是可配置的,就可使用defineProperty(..)方法来修改属性描述符:
var myObject = {
a: 3
}
myObject.a = 3;
myObject.a; // 3
Object.defineProperty(myObject,'a', {
value: 4,
writable: true,
configurable: false, // 不可配置
enumerable: true
})
myObject.a; //4
myObject.a = 5;
myObject.a; // 5
Object.defineProperty(myObject,'a', {
value: 6,
writable: true,
configurable: true,
enumerable: true
}); // TypeError
复制代码
最后一个defineProperty(..)会产生一个TypeError错误,无论是否是处于严格模式,尝试修改一个不可配置的属性描述符都会出错,注意:把configurable修改为false是单向操做,没法撤销!
有一个小小的例外,即使属性是configurable: false,
咱们仍是能够把writable状态由true改成false,可是没法由false改成true。
复制代码
除了没法修改,configurable: false还会禁止删除这个属性:
var myObject = {
a: 2
}
myObject.a; // 2
delete myObject.a;
myObject.a; // undefined
Object.defineProperty(myObject, 'a', {
value: 2,
writable: true,
configurable: false,
enumerable: true
})
myObject.a; // 2
delete myObject.a;
myObject.a; // 2
复制代码
这个描述符控制的是属性是否会出如今对象的属性枚举中,好比说for..in循环。若是把enumerable设置成false,这个属性就不会出如今枚举中,虽然仍然能够正常访问它,相对地,设置成true就会让它出如今枚举中。
有时候你会但愿属性或者对象是不可改变(不管是有意仍是无心)的,在ES5中能够经过不少种方法来实现。
很重要的一点是,全部的方法建立的都是浅不变性,也就是说,它们只会影响目标对象和它的直接属性。若是目标对象引用了其余对象(数组、对象、函数等),其余对象的内容不受影响,仍然是可变的:
myImmutableObject.foo; // [1,2,3];
myImmutableObject.foo.push(4);
myImmutableObject.foo; // [1,2,3,4]
复制代码
假设代码中的myImmutableObject已经被建立并且是不可变的,可是为了保护它的内容myImmutableObject.foo,你还须要使用下面的方法让foo也不可变。
结合writable:false和configurable:false就能够建立一个真正的常量属性(不可修改、重定义或者删除):
var myObject = {};
Object.defineProperty(myObject,"FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
})
复制代码
若是你想禁止一个对象添加新属性而且保留已有属性,可使用Object.preventExtensions(..):
var myObject = {
a: 2
}
Object.preventExtensions(myObject);
myObject.b = 3;
myObject.b; // undefined
复制代码
Object.seal(..)会建立一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..)并把全部现有属性标记为configurable:false。
因此,密封以后不只不能添加新属性,也不能从新配置或者删除任何现有属性(虽然能够修改属性的值)。
Object.freeze(..)会建立一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..)并把全部“数据访问”属性标记为writable:false,这样就没法修改它们的值。
这个方法是你能够应用在对象上的级别最高的不可变性,它会禁止对于对象自己及其任意直接属性的修改(不过就像咱们以前说过的,这个对象引用的其余对象是不受影响的)。
你能够“深度冻结”一个对象,具体方法为,首先在这个对象上调用Object.freeze(..),而后遍历它引用的全部对象并在这些对象上调用Object.freeze(..)。可是必定要当心,由于这样可能会在无心中冻结其余(共享)对象。
属性访问在实现时有一个微妙却很是重要的细节,思考下面的代码:
var myObject = {
a: 2
}
myObject.a; // 2
复制代码
myObject.a是一次属性访问,可是这条语句并不只仅是在myObject中查找名字为a的属性,虽然看起来好像是这样。
在语言规范中,myObject.a在myObject其实是实现了[[Get]]操做。对象默认的内置的[[Get]]操做首先在对象中查找是否有名称相同的属性,若是找到就会返回这个属性的值。
然而,若是没有找到名称相同的属性,按照[[Get]]算法的定义会执行另一种很是重要的行为。遍历可能存在的[[prototype]]链,也就是原型链。
若是不管如何都没有找到名称相同的属性,那么[[Get]]操做会返回值undefined:
var myObject = {
a: 2
}
myObject.b; // undefined
复制代码
注意,这种方法和访问变量时是不同的。若是你引用了一个档期词法做用域中不存在的变量,并不会像对象属性同样返回undefined,而是会抛出一个ReferenceError异常:
var myObject = {
a: undefined
}
myObject.a; // undefined
myObject.b; // undefined
复制代码
从返回值的角度来讲,这两个引用没有什么区别--它们都返回了undefined。然而,实际上底层的[[Get]]操做对myObject.b进行了更复杂的处理。
既然有能够获取属性值的[[Get]]操做,就必定有对应的[[Put]]操做。
[[put]]被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。
在ES5中可使用getter和setter部分改写默认操做,可是只能应用在单个属性上,没法应用在整个对象上。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。
var myObject = {
// 给a定义一个getter
get a() {
return 2;
}
}
Object.defineProperty(myObject,'b',{
// 给b设置一个getter
get: function() { return this.a *2 },
// 确保b会出如今对象的属性列表中
enumerable: true
})
myObject.a; // 2
myObject.b; // 4
复制代码
无论是对象文字语法中的get a() {...},仍是defineProperty(..)中的显式定义,两者都会在对象中建立一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被看成属性访问的返回值:
var myObject = {
// 给a定义一个getter
get a() {
return 2;
}
}
myObject.a = 3;
myObject.a; //2
复制代码
一般来讲,getter和setter是成对出现的(只定义一个的话一般会产生意料以外的行为):
var myObject = {
// 给a定义一个getter
get a() {
return this._a_;
},
set a(val) {
this._a_ = val * 2
}
}
myObject.a = 2;
myObject.a; // 4
复制代码
前面咱们说过,如myObject.a的属性访问返回值多是undefined,可是这个值有多是属性中存储的undefined,也多是由于属性不存在因此返回undefined。那么如何区分这两种状况呢?
var myObject = {
a: 2
}
console.log('a' in myObject); // true
console.log('b' in myObject); // false
myObject.hasOwnProperty('a'); // true
myObject.hasOwnProperty('b'); // false
复制代码
in操做符会检查属性名是否在对象及其[[Prototype]]原型链中。相比之下,hasOwnProperty(..)只会检查属性是否在myObject对象中,不会检查[[Prototype]链。
全部的普通对象均可以经过Object.prototype的委托来访问hasOwnPeoperty(..),可是有的对象可能没有链接到Object.prototype,(经过Object.create(null)来建立)。在这种状况下,形如myObject.hasOwnProperty(..)就会失败。
这时可使用一种更增强硬的方法来进行判断:Object.prototype.hasOwnProperty.call(myObject,'a'),它借用基础的hasOwnProperty(..)方法并把它显式绑定到myObject上。
var myObject = {};
Object.defineProperty(myObject,'a',{
value: 2,
// 让a像普通属性同样能够枚举
enumerable: true
})
Object.defineProperty(myObject,'b', {
value: 3,
// 让b不可枚举
enumerable: false
})
myObject.b; // 3
('b' in myObject); // true
myObject.hasOwnProperty('b'); // true
for(var k in myObject) {
console.log(k,myObject[k])
}
// 'a' 2
复制代码
能够看到,myObject.b确实存在而且有访问值,可是却不会出如今for..in循环中(尽管能够经过in操做符来判断是否存在)。缘由是“可枚举”就至关于“能够出如今对象属性的遍历中”。
也能够经过另外一种方式来区分属性是否可枚举:
var myObject = { };
Object.defineProperty(myObject,'a',{
enumerable: true,
value: 2
})
Object.defineProperty(myObject,'b',{
enumerable: false,
value: 3
})
myObject.propertyIsEnumerable('a'); // true
myObject.propertyIsEnumerable('b'); // false
Object.keys(myObject); // ['a']
Object.getOwnPropertyNames(myObject); // ['a','b']
复制代码
propertyIsEnumerable(..)会检查给定的属性名是否直接存在于对象中(而不是原型链上)而且知足enumerable:true。
Object.keys(..)会返回一个数组,包含全部可枚举属性,Object.getOwnPropertyNames(..)会返回一个数组,包含全部属性,不管它们是否可枚举。
in和hasOwnProperty(..)的区别在因而否查找[[Prototype]]链,然而,Object.keys(..)和Object.getOwnPropertyNames(..)都只会查找对象直接包含的属性。
for..in循环能够用来遍历对象的可枚举属性列表(包括[[Prototype]]链)。可是如何遍历属性的值呢?
对于数值索引的数组来讲,可使用标准的for循环来遍历值:
var myArray = [1,2,3];
for(var i = 0; i < myArray.length; i++){
console.log(myArray[i]);
}
// 1 2 3
复制代码
这实际上并非在遍历值,而是遍历下标来指向值,如myArray[i]。
ES5中增长了一些数组的辅助迭代器,包括forEach(..)、every(..)和some(..)。每种辅助迭代器均可以接受一个回调函数并把它应用到数组的每一个元素上,惟一的区别就是它们对于回调函数返回值的处理方式不一样。
那么如何直接遍历值而不是数组下标(或者对象属性)呢?幸亏,ES6增长了一种用来遍历数组的for..of循环语法(若是对象自己定义了迭代器的话也能够遍历对象):
var myArray = [1,2,3];
for(var v of myArray) {
console.log(v);
}
// 1 2 3
复制代码
for..of循环首先会向被访问对象请求一个迭代器对象,而后经过调用迭代器对象的next()方法来遍历全部返回值。
JavaScript中的对象有字面形式(好比var a = {..})和构造形式(好比var a = new Array(..))。字面形式更经常使用,不过有时候构造形式能够提供更多选项。
许多人都觉得“JavaScript中万物都是对象”,这是错误的。对象是6个(或者是7个,取决于你的观点)基础类型之一。对象有包括function在内的子类型,不一样子类型具备不一样的行为,好比内部标签[Object Array]表示这是对象的子类型数组。
对象就是键值对的集合。能够经过.propName或者['propName']语法来获取属性值。访问属性时,引擎实际上会调用内部的默认[[Get]]操做(在设置属性值时是[[Put]]),[[Get]]操做会检查对象自己是否包含这个属性,若是没找到的话,还会查找[[Prototype]]链。
属性的特性能够经过属性描述符来控制,好比writable和configurable。此外,可使用Object.preventExtensions(..)、Object.seal(..)和Object.freeze(..)来设置对象(及其属性)的不可变性级别。
属性不必定包含值--它们多是具有getter/setter的“访问描述符”。此外,属性能够是枚举或者不可枚举的,这决定了它们是否会出如今for..in循环中。
你可使用ES6的for..of语法来遍历数据结构(数组、对象,等等)中的值,for..of会寻找内置或者自定义的@@iterator对象并调用它的next()方法来遍历数据值。
感受很久没有更新了,一直断断续续的,也是由于前段时间工做有点忙,没办法平衡工做和学习的时间,如今终于能够习惯一些,因此开始坚持把这一个系列的书好好看完。