对象可以通过两种形式定义:声明(文字)形式和构造形式。
对象的文字语法如下:
var myobj = {
key: value
// ...
}
构造形式大概是这样的:
var myobj = new Object()
myObj.key = value
构造形式和文字形式生成的对象是一样的。唯一的区别是,文字形式中你可以添加多个键值对,而构造形式中你必须逐个添加属性。
在JavaScript中一共有六种主要类型(术语是”语言类型“):
注意:简单基本类型(string、boolean、number、null和undefined)本身并不是对象。对null执行typeof null 时会返回字符串“object”,是因为不同的对象在底层都表示为二进制,在JavaScript中二进制前三位都为0的话就会被判断为object类型,null的二进制表示全为零,所以执行typeof时会返回"object"
JavaScript中还有一些对象子类型,通常被称为内置对象。他们实际上只是一些内置函数,这些内置函数可以当做构造函数(new产生的函数)来使用,从而可以构造一个子类型的新对象。
对于“I am a string”这样一个原始值,她并不是一个对象,而是一个字面量。如果要在这个字面量上进行一些操作,比如获取长度,访问其中某个字符,引擎就会自动把字面量转换成String对象,从而可以访问其属性。null 和 undefined 没有对应的构造形式,Date只有构造形式。
访问一个对象时,我们可以通过 obj.a 的形式来访问,也可以通过 obj[“a”] 的形式来访问。除了这两种以外,ES6 还增加了可计算属性名,可以在文字形式中使用 [ ] 包裹一个表达式来当做属性名:
var prefix = "foo"
var myObject = {
[prefix + "bar"]: "hello"
[prefix + "baz"]: "world"
}
myObject["foobar"]; // hello
myObject["foobaz"]; // world
可计算属性名最常用的场景可能是ES6 的符号(Symbol)。
数组也支持 [ ] 访问形式。无论是通过 . 语法还是 [ ] 语法,数组都是键与值相同。或者通过数组特有的下标也可以进行访问。
我们主要需要了解深拷贝与浅拷贝,对于浅拷贝,新对象只会复制旧对象的值,对于函数对象,则会采用引用的方式。对于深拷贝,会遍历一个或多个源对象的所有可枚举的自有键并把它们复制到目标对象,最后返回目标对象。
深拷贝的实现:
在 jQuery 中有一个叫 $.clone() 的方法,可是它并不是用于一般的 JS 对象的深复制,而是用于 DOM 对象。除此之外,我们也是可以通过 . e x t e n d ( ) 方 法 来 完 成 深 复 制 。 我 们 在 j Q u e r y 中 可 以 通 过 添 加 一 个 参 数 来 实 现 ∗ ∗ 递 归 e x t e n d ∗ ∗ , 调 用 ‘ .extend() 方法来完成深复制。我们在 jQuery 中可以通过添加一个参数来实现**递归extend**,调用` .extend()方法来完成深复制。我们在jQuery中可以通过添加一个参数来实现∗∗递归extend∗∗,调用‘.extend(true, {}, …)`就可以实现深复制啦。
在lodash中关于复制的方法有两个,分别是_.clone()
和_.cloneDeep()
。其中_.clone(obj, true)
等价于_.cloneDeep(obj)
。使用上,lodash和前两者并没有太大的区别,但看了源码会发现,Underscore 的实现只有30行左右,而 jQuery 也不过60多行。可 lodash 中与深复制相关的代码却有上百行,这是什么道理呢?
jQuery 无法正确深复制 JSON 对象以外的对象,而 lodash 花了大量的代码来实现 ES6 引入的大量新的标准对象。更厉害的是,lodash 针对存在环的对象的处理也是非常出色的。因此相较而言,lodash 在深复制上的行为反馈比前两个库好很多,
相比于上面介绍的三个库的做法,针对纯 JSON 数据对象的深复制,使用 JSON 全局对象的 parse
和 stringify
方法来实现深复制也算是一个简单讨巧的方法。然而使用这种方法会有一些隐藏的坑,它能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。
function jsonClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
var clone = jsonClone({
a:1 });
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KGzWaDsO-1620399569986)(C:\Users\DSH\Pictures\博客\第三方实现深拷贝的对比.png)]
在ES5之前, JavaScript 语言本身并没有提供可以直接检测属性特性的方法,比如判断属性是否只读。
但是从 ES5 开始,所有属性都具备了属性描述符。
一个对象的属性描述符包括value、writable(可写)、enumerable(可枚举)和configurable(可配置)。
在创建普通属性时属性描述符会使用默认值,我们也可以使用Object.defineProperty(…)来添加一个新属性或者修改一个已有属性(如果它是configurable)并对特性进行设置。
举例来说:
var myObject = {
};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
})
myObject.a // 2
我们来介绍一下各属性描述符的作用:
writable
writable 决定是否可以修改属性的值。
configurable
只要属性是可配置的,就可以使用 defineProperty(…) 方法来修改属性描述符。但是把 configurable 修改为 false 是单向操作,至于为什么就不用我说了吧。
enumerable
这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说 for…in 循环。如果把 enumerable 设置为 false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。
所有的方法创建的都是浅不变性,也就是说,它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数,等),其他对象和内容不受影响,仍然是可变的:
myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push(4)
myImmutableObject.foo; // [1,2,3,4]
假设代码中的myImmutableObject 已经被创建而且是不可变的,但是为了保护它的内容myImmutableObject.foo,你还需要使用下面的方法让foo 也不可变。
对象常量
结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性。
禁止扩展
如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(…) :
var myObject = {
a: 2
}
Object.preventExtensions( myObject )
myObject.b = 3
myObject.b // undefined
在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。
密封
Object.seal(…) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(…) 并把所有现有属性标识为 configurable:false。
所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。
冻结
Object.freeze(…) 会创建一个冻结的对象,这个方法实际上会在一个现有对象上调用Object.seal(…) 并把所有"数据访问"属性标志为 writable:false 这样就无法修改它们的值。
这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象 引用的其他对象是不受影响的)。
你可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用 Object.freeze(…) ,然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(…) 。但是一定要小心,因为这样做有可能会在无意中冻结其他(共享)对象。
在语言规范中,.属性名实际上实现了[[Get]]操作(有点像函数调用:[[Get]]())。对象默认内置[[Get]]操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。然而,如果没有找到,就会执行遍历原型链的操作。
如果无论如何都没有找到名称相同的属性,那[[Get]]操作会返回值 undefined。
[[Put]]被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性。
如果已经存在这个属性,[[put]] 算法大致会检查下面这些内容。
如果对象中不存在这个属性,[[put]] 操作会更加复杂。我们会在讲 [[Prototype]] 详细介绍。
对象默认的 [[Get]] 和 [[Put]] 操作分别可以控制属性值的设置和获取。
在 ES5 中可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象中。getter 是一个隐藏函数,会在获取属性值时调用。setter 也是一个隐藏函数,会在设置属性值时调用。
但你给一个属性定义 getter 、setter或者两者都有时,这个属性会被定义为“访问 描述符”(和“数据描述符”相对)。对于访问描述符来说,JavaScript 会忽略它们的 value 和 writable 特性,取而代之的是关心 set 和 get (还有 configurable 和 enumerable)的特性。
思考下面代码:
var myObject = {
// 给a定义一个getter
get a(){
return 2
}
}
Object.defineProperty(
myObject, // 目标属性
"b", // 属性名
{
// 描述符
// 给b设置一个getter
get: function(){
return this.a * 2 },
// 确保b会出现在对象的属性列表中
enumerable: ture
}
)
myObject.a // 2
myObject.b // 4
不管是对象文字语法中的 get a( ) { … }, 还是defineProperty( … ) 中的显示定义,二者都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐式函数,它的返回值会被当做属性访问的返回值:
var myObject = {
// 给a定义一个getter
get a() {
return 2
}
}
myObject.a = 3
myObject.a // 2
setter同理,会在进行赋值操作的时候调用setter函数。
前面我们说过,如myObject.a的属性访问返回值可能是undefined但是这个值有可能是属性中存储的 undefined,也可能是因为属性不存在所以返回 undefined。那么如何区分这两种情况呢?
我们可以在不访问属性值的情况下判断是否存在这个属性:
var myObject = {
a: 2
}
("a" in myObject) //ture
("b" in myObject) //false
myObject.hasOwnProperty( "a" ) //ture
myObject.hasOwnProperty( "b" ) //false
in 操作符会检查属性是否存在对象及其 [[Prototype]] 原型链中。相比之下,hasOwnProperty( … )只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。
所有的普通对象都可以通过对于 Object.prototype 的委托来访问 hasOwnProperty( … ),但是有的对象可能没有连接到 Object.prototype。在这种情况下,形如 myyObject.hasOwnProperty( … ) 就会失败。这时可以使用一种更加强硬的方法来进行判断:Object.p.hasOwnProperty.call(myObject, “a”),它借用基础的 hasOwnProperty( … )方法并把它显示绑定到myObject上。
什么 可枚举性?简单来说,就是可以出现在对象属性的遍历中。
和枚举有关的方法有:
propertyIsEnumerable( … )会检查给定的属性名是否直接存在于对象中(而不是原型链上)并且满足 enumerable:true。
Object.keys(…)会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames( … ) 会返回一个数组,包含所有属性,无论他们是否可枚举。
in 和 hasOwnProperty( … ) 的区别在于是否查找 [[Prototype]] 链,然而, Object.keys(…) 和 Object.getOwnPropertyNames( … ) 都只会查找对象直接包含的属性。
(目前)没有内置的方法可以获取 in 操作符使用的属性列表(对象本身的属性以及 [[Prototype]] 链中所有属性)。不过你可以递归遍历某个对象的整条 [[Prototypr]] 链并保存每一层中使用 Object.keys(…)得到的属性列表——只包含可枚举属性。
for…in 循环可以用来对象的可枚举属性列表(包括[[Prototype]] 链)。但是如何遍历属性的值呢?
最普通的方法就是使用for循环。但for循环实际上并不是在遍历值,而是通过遍历下标来指向值。
ES5中增加了一些数组的辅助迭代器,包括 forEach( … )、 every( … )、some( … )。每种辅助迭代器都可以接受一个回调函数并把它应用到数组的每个元素上,唯一的区别就是他们对于回调函数返回值的处理方式不同。
forEach( … ) 会遍历数组中所有值并忽略回调函数的返回值。every( … )会一直运行知道回调函数返回 false(或者“假”值),some( … )会一直运行直到回调函数返回true(或者“真”值)。
every( … ) 和 some( … ) 中特殊的返回值和普通 for 循环中的 break 语句类似,他们会提前终止遍历。
使用for…in 遍历对象是无法直接获取属性值的,因为他实际上遍历的是对象中的所有可枚举对象,你需要手动获取属性值。
ES6增加了一种用来遍历数组的for… of循环语法(如果对象本身定义了迭代器的话也可以遍历对象):
var myArray = [ 1, 2, 3]
for (var v of myArray) {
console.log( v )
}
// 1
// 2
// 3
for…of 循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的 next() 方法来遍历所有返回值。
数组有内置的 @@iterator, 因此 for…of 可以在应用在数组上。我们使用内置的 @@iterator 来手动遍历数组:
var myArray = [ 1, 2, 3 ]
var it = myArray[Symbol.iterator]() // 我们使用ES6中的符号 Symbol.iterator 来获取对象的 @@iterator 内部属性。
it.next() //{ value:1, done:false }
it.next() //{ value:2, done:false }
it.next() //{ value:3, done:false }
it.next() //{ done:true }
迭代器的next() 方法会返回value 表示单前的遍历值,done是一个布尔值,表示是否还有可以遍历的值。
由于对象中并没有内置@@iterator,所以我们可以在对象中return 一个next() 方法来定义@@iterator
return {
next: function() {
return {
value: o[ks[idx++]],
done: (idx > ks.lenght)
}
}
}
next() //{ value:2, done:false }
it.next() //{ value:3, done:false }
it.next() //{ done:true }
迭代器的next() 方法会返回value 表示单前的遍历值,done是一个布尔值,表示是否还有可以遍历的值。
由于对象中并没有内置@@iterator,所以我们可以在对象中return 一个next() 方法来定义@@iterator
```js
return {
next: function() {
return {
value: o[ks[idx++]],
done: (idx > ks.lenght)
}
}
}
当然你可以return一个随机数来形成一个无限迭代器。