【对象】。

语法

一、对象可以通过两种形式定义:声明(文字)形式和构造形式。

  1. 声明(文字)形式
var myObj = {
	key: value,
	// ...
};
  1. 构造形式
var myObj = new Object();
myObj.key = value;
//...

二、二者区别:

  1. 在文字声明中你可以添加多个键/值对,但是在构造形式中你必须逐个添加属性。
  2. 在vm处理过程中,构造形式会先搜索作用域链查找名为Object的对象,然后再进行构造调用,声明形式不会有查找过程

类型

一、JavaScript中有许多特殊的对象子类型,我们可以称之为复杂基本类型:函数、数组
二、JavaScript中还有一些对象子类型,通常被称为内置对象:StringNumberBooleanObjectFunctionArrayDateRegExpError
三、内置函数可以当作构造函数来使用,从而可以构造一个对应子类型的新对象

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

Object.prototype.toString.call(strObject);// [object String]

四、原始值"I am a string"并不是一个对象,它只是一个字面量,并且是一个不可变的值。如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要将其转换为String对象。幸好,在必要时语言会自动把字符串字面量转换成一个String对象,但这个都西昂是个临时对象,会在API调用后立即销毁,我们每次调用API都在操作一个新的临时对象。

var strPrimitive = "I am a string";
console.log(strPrimitive.length);// 13
console.log(strPrimitive.charAt);// "m"

使用以上两种方法,我们都可以直接在字符串字面量上访问属性或者方法,之所以可以这样做,是因为引擎自动把字面量转换成String对象,所以可以访问属性和方法。
五、数值字面量、布尔字面量也同样如此,如果使用类似42.359.toFixed(2)的方法,引擎会把42转换成new Number(42)
六、nullundefined没有对应的构造形式,它们只有文字形式。
七、Date只有构造,没有文字形式。
八、ObjectArrayFunctionRegExp(正则表达式)无论使用文字形式还是构造形式,它们都是对象,不是字面量
九、Error对象很少在代码中显式创建,一般是在抛出异常时被自动创建
十、null有时会被当作一种对象类型,但是这其实只是语言本身的一个bug,即typeof null返回字符串"object",而null本身是基本类型。这是因为不同的对象在底层都表示为二进制,在JavaScript中二进制前三位都为0的话会被判断为object类型,null的二进制表示全是0,自然前三位也是0,所以被误判为object类型。

内容

一、对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性。
二、在引擎内部,这些值一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们就像指针一样,指向这些值真正的存储位置。
三、如果要访问myObject中a位置上的值,我们需要使用.操作符或者[]操作符。.a语法通常被称为“属性访问”, ["a"]语法通常被称为“键访问”。这两种语法的主要区别在于.操作符要求属性名满足标识符的命名规范,而[".."]语法可以接受任意UTF-8/Unicode字符串作为属性名

var myObject = {
	a: 2
};
myObject.a;// 2
myObject["a"];// 2

四、由于[".."]语法使用字符串来访问属性,所以可以在程序中构造字符串用以访问属性:

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[myObject] = "baz";
myObject["true"];// "foo"
myObject["3"];// "bar"
myObject["[object Object]"];// "baz"

可计算属性名

一、ES6增加了可计算属性名,可以在文字形式中使用[]包裹一个表达式来当作属性名

var prefix = "foo";
var myObject = {
	[prefix + "bar"]:"hello",
	[prefix + "baz"]:"world"
};
myObject["foobar"];// hello
myObject["foobaz"];// world

二、可计算属性名最常用的场景可能是ES6的符号(Symbol)

属性与方法

一、对象的方法不属于对象本身,方法本身是对一个函数指针的引用。
二、从技术角度来说,函数永远不会“属于”一个对象
三、属性访问返回的函数和其他函数没有任何区别,除了可能发生的隐式绑定this
四、即使你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象——它们只是对于相同函数对象的多个引用
五、即使你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象——它们只是对于相同函数对象的多个引用

数组

一、数组也是对象,所以虽然每个下标都是整数,你仍然可以给数组添加属性,单就算添加了命名属性(无论是通过.语法还是[]语法),数组的length值并未发生变化

var myArray = ["foo", 42, "bar"];
myArray.baz = "baz";
myArray.length;// 3
myArray.baz;// "baz"

二、最好只用对象来存储键/值对,只用数组来存储数值下标/值对。
三、如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值下标(因此会修改数组的内容而不是添加一个属性)

var myArray = ["foo", 42, "bar"];
myArray["3"] = "baz";
myArray.length;// 4
myArray[3];// "baz"

复制对象

function anotherFunction(){}
function anotherObject = {
	c: true
};
var anotherArray = [];
var myObject = {
	a: 2,
	b: anotherObject,// 引用,不是复制
	c: anotherArray,// 另一个引用
	d: anotherFunction
}
anotherArray.push(anotherObject, myObject)

说明:如何准确地表示myObject的复制呢?首先,我们应该判断它是浅复制还是深复制。对于浅拷贝来说,复制出的新对象中a的值会复制旧对象中a的值,也就是2,但是新对象中bcd三个属性其实只是三个引用,它们和旧对象中bcd引用的对象是一样的。对于深复制来说,除了复制myObject以外还会复制anotherObjectanotherArray。这时问题就来了,anotherArray引用了anotherObjectmyObject,所以又需要复制myObject,这样就会由于循环引用导致死循环。
一、对于JSON安全(也就是说可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:var newObj = JSON.parse(JSON.stringify(someObj));
二、ES6定义了Object.assign(..)方法来实现浅复制。该方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举(enumerable)的自有键(owned key)并把它们复制(使用=操作符赋值)到目标对象,最后返回目标对象
三、由于Object.assign(..)就是使用=操作符来赋值,所以源对象属性的一些特性(比如writable)不会被复制到目标对象

属性描述符

一、所有的属性都具备了属性描述符。

  1. writable决定是否可以修改属性的值。你可以把writable:false看作是属性不可改变,相当于你定义了一个空操作setter
  2. Configurable只要属性是可配置的,就可以使用defineProperty(..)方法来修改属性描述符。把configurable修改成false是单向操作,无法撤销!即便属性是configurable:false,我们还是可以把writable的状态由true改为false,但是无法由false改为true。除了无法修改,configurable:false还会禁止删除这个属性
  3. Enumerable控制的是属性是否会出现在对象的属性枚举中,比如说for..in循环。如果把enumerable设置成false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。相对地,设置成true就会让它出现在枚举中。
var myObject = {a: 2};
Object.getOwnPropertyDescriptor(myObject, "a");
/*
{
	value: 2,
	writable: true,
	enumerable: true,
	configurable: true
}
*/

二、可以使用Object.defineProperty(..)来添加一个新属性或者修改一个已有属性(如果它是configurable)并对特性进行设置

不变性

[[Get]]

一、

var myObject = {a: 2};
myObject.a;// 2

myObject.amyObject上实际上是实现了[[Get]]操作(有点像函数调用:[[Get]]())。对象默认的内置[[Get]]操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。然而,如果没有找到名称相同的属性,则去原型链上查找。如果无论如何都没有找到名称相同的属性,那[[Get]]操作会返回值undefined
二、如果你引用了一个当前词法作用域中不存在的变量,并不会像对象属性一样返回undefined,而是会抛出一个ReferenceError异常

[[Put]]

对象中是否已经存在这个属性是触发[[Put]]最重要的因素。如果已经存在这个属性,会检查下面这些内容。

  1. 属性是否是访问描述符?如果是并且存在setter就调用setter
  2. 属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,在严格模式下抛出TypeError异常。
  3. 如果都不是,将该值设置为属性的值。

如果对象中不存在这个属性,[[Put]]操作会更加复杂。我们会在后面讨论[[Prototype]]时详细进行介绍。

Getter和Setter

一、对象默认的[[Put]][[Get]]操作分别可以控制属性值的设置和获取。
二、在ES5中可以使用gettersetter部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。
三、当你给一个属性定义gettersetter或者两者都有时,这个属性会被定义为“访问描述符”
四、对于访问描述符来说,JavaScript会忽略它们的valuewritable特性,关心setget(还有configurableenumerable)特性。
五、setter会覆盖单个属性默认的[[Put]](也被称为赋值)操作

存在性

var myObject = {
	a: undefined
};
myObject.a;// undefined
myObject.b;// undefined

myObject.a的属性访问返回值可能是undefined,但是这个值有可能是属性中存储的undefined,也可能是因为属性不存在所以返回undefined。那么如何区分这两种情况呢?

var myObject = {
	a: undefined
};
("a" in myObject);// true
("b" in myObject);// false
myObject.hasOwnProperty("a");// true
myObject.hasOwnProperty("b");// false

in操作符会检查属性是否在对象及其[[Prototype]]原型链中。hasOwnProperty(..)只会检查属性是否在myObject对象中,不会检查[[Prototype]]链。
所有的普通对象都可以通过对于Object.prototype的委托来访问hasOwnProperty(..),但是有的对象可能没有连接到Object.prototype(通过Object. create(null)来创建)。在这种情况下,形如myObejct.hasOwnProperty(..)就会失败。这时可以使用一种更加强硬的方法来进行判断:Object.prototype.hasOwnProperty. call(myObject, "a"),它借用基础的hasOwnProperty(..)方法并把它显式绑定到myObject上。
看起来in操作符可以检查容器内是否有某个值,但是它实际上检查的是某个属性名是否存在。对于数组来说这个区别非常重要,4 in [2, 4, 6]的结果并不是你期待的True,因为[2, 4, 6]这个数组中包含的属性名是012,没有4

枚举

一、考虑以下代码:

Object.defineProperty(
	myObject,
	"a",
	{enumerable: true, value: 2}// 让a像普通对象一样可枚举
);
Object.defineProperty(
	myObject,
	"b",
	{enumerable: false, value: 3}// 让b不可枚举
);
myObject.b;// 3
("b" in myObject);// true
myObject.hasOwnProperty("b");// true
for(var k in myObject){
	console.log(k,myObject[k]);// "a" 2
}

说明:

  1. myObject.b确实存在并且有访问值,但是却不会出现在for..in循环中(尽管可以通过in操作符来判断是否存在)。原因是“可枚举”就相当于“可以出现在对象属性的遍历中”。
  2. 在数组上应用for..in循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性。最好只在对象上应用for..in循环,如果要遍历数组就使用传统的for循环来遍历数值索引。

二、也可以通过另一种方式来区分属性是否可枚举:

Object.defineProperty(
	myObject,
	"a",
	{enumerable: true, value: 2}// 让a像普通对象一样可枚举
);
Object.defineProperty(
	myObject,
	"b",
	{enumerable: false, value: 3}// 让b不可枚举
);
myObject.propertyIsEnumerable("a");// true
myObject.propertyIsEnumerable("b");// false
Object.keys(myObject);// ["a"]
Object.getOwnPropertyNames(myObject);// ["a", "b"]

说明:

  1. propertyIsEnumerable(..)会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true
  2. Object.keys(..)会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..)会返回一个数组,包含所有属性,无论它们是否可枚举
  3. inhasOwnProperty(..)的区别在于是否查找[[Prototype]]链,然而,Object.keys(..)Object.getOwnPropertyNames(..)都只会查找对象直接包含的属性。
  4. 没有内置的方法可以获取in操作符使用的属性列表(对象本身的属性以及[[Prototype]]链中的所有属性)。不过你可以递归遍历某个对象的整条[[Prototype]]链并保存每一层中使用Object.keys(..)得到的属性列表——只包含可枚举属性。

遍历

一、for循环实际上并不是在遍历值,而是遍历下标来指向值
二、forEach(..)every(..)some(..)唯一的区别就是它们对于回调函数返回值的处理方式不同。forEach(..)会遍历数组中的所有值并忽略回调函数的返回值。every(..)会一直运行直到回调函数返回false(或者“假”值), some(..)会一直运行直到回调函数返回true(或者“真”值)。
三、使用for..in遍历对象是无法直接获取属性值的,因为它实际上遍历的是对象中的所有可枚举属性,你需要手动获取属性值。遍历对象属性时的顺序是不确定的,在不同的JavaScript引擎中可能不一样
四、那么如何直接遍历值而不是数组下标(或者对象属性)呢?ES6增加了一种用来遍历数组的for..of循环语法(如果对象本身定义了迭代器的话也可以遍历对象):

var myArray = [1, 2, 3];
for(var v of myArray){
	console.log(v);
}

for…of循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值。数组有内置的@@iterator,因此for…of可以直接应用在数组上。我们使用内置的@@iterator来手动遍历数组

var myArray = [1, 2, 3];
var it = myArray[Symbol.iterator]();
it.next();// {value: 1, done: false}
it.next();// {value: 2, done: false}
it.next();// {value: 3, done: false}
it.next();// {done: true}

说明:

  1. 我们使用ES6中的符号Symbol.iterator来获取对象的@@iterator内部属性。
  2. 引用类似iterator的特殊属性时要使用符号名,而不是符号包含的值。
  3. @@iterator本身并不是一个迭代器对象,而是一个返回迭代器对象的函数

五、和数组不同,普通的对象没有内置的@@iterator,所以无法自动完成for…of遍历
六、你可以给任何想遍历的对象定义@@iterator:

var myObject = {
	a: 2,
	b: 3
};
Object.defineProperty(myObject, Symbol.iterator, {
	enumerable: false,
	writable: false,
	configurable: true,
	value: function(){
		var o = this;
		var idx = 0;
		var ks = Object.keys(o);
		return {
			next: function() {
				return {
					value: o[ks[idx++]],
					done: (idx > ks.length)
				};
			}
		};
	}
});
// 手动遍历myObject 
var it = myObject[Symbol.iterator]();
it.next();//{value: 2, done: false}
it.next();//{value: 3, done: false}
it.next();//{value: undefined, done: true}
// 用for...of遍历myObject
for(var v of myObject) {
	console.log(v);
}

说明:

  1. 我们使用Object.defineProperty(..)定义了我们自己的@@iterator(主要是为了让它不可枚举),不过注意,我们把符号当作可计算属性名。此外,也可以直接在定义对象时进行声明,比如var myObject= {a:2, b:3, [Symbol.iterator]: function() { /* .. */ } }。
  2. for..of循环每次调用myObject迭代器对象的next()方法时,内部的指针都会向前移动并返回对象属性列表的下一个值

类理论

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值