详解JS中的对象

1、语法

对象的声明有两种形式,声明(文字)形式和构造形式

// 文字形式
var mtObj = {
    key:value
    ...
}
// 构造形式
var myObj = new Object()
myObj.key = value

构造形式与文字形式生成的对象是一样的,唯一的区别在于文字形式可以添加多个键值对,但是在构造形式中必须逐个添加属性

2、类型

对象是JS的基础,在JS中共有六种主要的类型:string、number、boolean、null、underfined、object

null有时会被当做一种对象类型,但实际上这是一个BUG,对null执行typeof null会返回字符串“object”,他的原理是因为不同的对象在底层都表示为二进制,在JS中二进制前三位为0的话会被判断为object类型,null的二进制全为0,所以执行typeof时会返回object

2.1、内置对象

JS中还有一些对象子类型,通常被称为内置对象,比如:String、Number、Boolean、Object、Function、Array、Date、RegExp、Error,这些内置对象从表现形式上来说很像其他语言类型中的类型或者类,但是在JS中,他们实际上只是一些内置函数,这些内置函数可以当做构造函数来使用,比如

var obj = new String("aaa")
typeof obj // object
obj instanceof String // true

我们可以直接在字符串字面量上访问属性或者方法,这是因为JS引擎自动把字面量转换为String对象

var a = "abcde"
console.log(a.length) // 5
console.log(a.charAt(3)) // d

3、对象的“内容”

对象的内容是由一些存储在特定命令位置的(任意类型的)值组成的,称之为属性。

这些值的存储方式是多种多样的,一般不会存在对象容器的内部,存储在对象容器内部的是这些属性的名称,它们就像指针,指向这些值的真正存储位置。

访问对象的属性时,我们可以使用.操作符或者[]操作符,前者被称为属性访问,后者为键访问,主要区别在于,.操作符要求属性名满足标识符的命名规范,而[..]语法可以接收UTF-8/Unicode字符串作为属性名。

在对象中,属性名永远都是字符串,即使是数字也不例外,虽然在数组下表标中使用的的确是数字,但是在对象属性名中数字会被转换为字符串

3.1、可计算属性名

如果需要通过表达式来计算属性名,那么就需要用到刚刚所说的[]属性访问语法

var a = "foo"
var obj = {
    [a + "b"]:'hello'
    [a + "c"]:'world'
}
obj["ab"] // hello
obj["ac"] // world

3.2、属性与方法

如果访问的对象属性是一个函数,有些开发者喜欢使用不一样的叫法来区分。由于函数很容易被认为是属于某个对象,在其他语言中,属于对象(也被称为“类”)的函数通常被称为方。

在JS中,通常来说函数和方法是可以互换的,虽然在ES6中新增的super关键字,super的行为似乎更有理由把super绑定的函数称为方法,但这只是一些语义上的微秒差别,本质上是一样的

3.3、数组

数组也是对象,所以虽然每个下标都是整数,但是仍然可以给数组添加属性

var obj = ["a", "b", "c"]
obj.d = "d"
console.log(obj.length); // 3
console.log(obj.d) // d
console.log(obj[3]); // underfined

3.4、属性描述符

从ES5开始,所有的属性都具备了属性描述符.

在创建普通函数时属性描述符会使用默认值,但是我们也可以使用Object.defineProperty(…)来添加一个新属性或者修改一个已有属性,比如说:

var myObject = {}
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
})
myObject.a // 2
1、writable

该属性决定是否可以修改属性的值

var myObject = {}
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: false, // 不可写
    configurable: true,
    enumerable: true
})
myObject.a = 3
myObject.a // 2

对于属性的值的静默修改失败了,如果是在严格模式下,会报错TypeError

2、configurable

只要属性是可配置的,就可以使用defineProperty(…)方法来修改属性描述符

var myObject = {
    a: 2
}
myObject.a = 3
console.log(myObject.a); // 3

Object.defineProperty(myObject, "a", {
    value: 4,
    writable: true,
    configurable: false, // 不可配置
    enumerable: true
})
console.log(myObject.a); // 4
myObject.a = 5
console.log(myObject.a); // 5

Object.defineProperty(myObject, "a", {
    value: 6,
    writable: true,
    configurable: true, // 不可配置
    enumerable: true
})
console.log(myObject.a); // TypeError

最后伊藤defineProperty(…)会产生一个TypeError错误,因为 尝试修改一个不可配置的描述符都会报错

但是即使属性是不可配置的,但是还是可以吧writable的状态由true改为false,但是无法由false改为true,可见,将configurable修改为false是单向操作,无法撤销

除了无法修改,configurable:false还会禁止删除这个属性

var myObject = {
    a: 2
}
console.log(myObject.a); // 2
delete myObejct.a
console.log(myObject.a); // undefined

Object.defineProperty(myObject, "a", {
    value: 4,
    writable: true,
    configurable: false, // 不可配置
    enumerable: true
})

myObject.a // 2
delete myObject.a
myObject.a // 2

最后一个delete语句失败了,因为属性是不可配置的,但是不要把delete看做一个释放内存的工具,它就是一个删除对象属性的操作,仅此而已

3、Enumerable

该属性控制的是属性是否会出现在对象的属性枚举中

3.5、不变性

有时候我们希望属性或者对象是不可改变的,在ES5中我们可以通过很多种方法来实现,但是,所有的方法创建的都是浅不变性,也就是说,他们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数),其他对象的内容不受影响,仍然是可变的

myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push( 4 );
myImmutableObject.foo; // [1,2,3,4]

假设上述代码中myImmutableObject被创建且是不可变的,但是为了保护它的内容myImmutableObject.foo,还需要使用以下方法让foo也不可变

  1. 对象常量

    使用上述所说的writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、重定义或者删除)

    var myObject = {};
    Object.defineProperty( myObject, "FAVORITE_NUMBER", {
    	value: 42,
    	writable: false,
    	configurable: false
    } );
    
  2. 禁止扩展

    如果想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(…)

    var myObject = {
    	a:2
    };
    Object.preventExtensions( myObject );
    myObject.b = 3;
    myObject.b; // undefined
    
  3. 密封

    Object.seal(…) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(…) 并把所有现有属性标记为 configurable:false。所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。

  4. 冻结

    Object.freeze(…)会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(…)并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们的值。
    这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。
    你可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用 Object.freeze(…),然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(…)。但是一定要小心,因为这样做有可能会在无意中冻结其他(共享)对象。

3.6、[[Get]]

看以下代码

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

看起来好像是执行myObject.a,查找名字为a的属性,但实际上是实现了[[Get]]操作,对象默认的内置[[Get]]操作首先在对象中查找是否有名称相同的属性,有的话就返回这个属性,没有的话就会遍历查找原型链,如果原型链也没有找到,就会返回undefined

3.7、[[put]]

[[Put]] 被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。如果已经存在这个属性,[[Put]] 算法大致会检查下面这些内容:

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

如果对象中不存在这个属性,[[Put]] 操作会更加复杂。

3.8、Getter和Setter

对象默认的[[Put]]和[[Setter]]操作分别可以控制属性值的设置和获取

当你给一个属性定义 getter、setter 或者两者都有时,这个属性会被定义为“访问描述符”(和“数据描述符”相对)。对于访问描述符来说,JavaScript 会忽略它们的 value 和writable 特性,取而代之的是关心 set 和 get(还有 configurable 和 enumerable)特性。

var myObject = {
    // 给 a 定义一个 getter
    get a() {
        return this._a_;
    },
    // 给 a 定义一个 setter
    set a(val) {
        this._a_ = val * 2;
    }
};
myObject.a = 2;
myObject.a; // 4

3.9、存在性

现在有一种情况,假如myObject.a的属性值返回undefined,那么我们如何确定这个值是属性中存储的还是该属性值不存在返回的呢

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

in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中,相比之下,hasOwnProperty(…) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。

也可以使用 一 种 更 加 强 硬 的 方 法 来 进 行 判 断:Object.prototype.hasOwnProperty.call(myObject,“a”),它借用基础的 hasOwnProperty(…) 方法并把它显式绑定到 myObject 上。

看起来 in 操作符可以检查容器内是否有某个值,但是它实际上检查的是某个属性名是否存在。对于数组来说这个区别非常重要,4 in [2, 4, 6] 的结果并不是你期待的 True,因为 [2, 4, 6] 这个数组中包含的属性名是 0、1、2,没有 4

接下来看一下枚举的相关知识:

var myObject = { };
Object.defineProperty(
	myObject,
	"a",
	// 让 a 像普通属性一样可以枚举
	{ enumerable: true, value: 2 }
);
Object.defineProperty(
	myObject,
	"b",
	// 让 b 不可枚举
	{ enumerable: false, value: 3 }
);
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",
	// 让 a 像普通属性一样可以枚举
	{ enumerable: true, value: 2 }
);
Object.defineProperty(
	myObject,
	"b",
	// 让 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(…) 都只会查找对象直接包含的属性。

4、遍历

for…in循环可以用来遍历对象的可枚举属性,但是如何遍历属性的值呢。

在ES5中新增了一些数组的辅助迭代器,包括forEach()、every()、some(),每种辅助迭代器都可以接受一个回调函数并把它们应用到数组的每个元素上,唯一的区别急速它们对于回调函数返回值的处理方式不同。

forEach(…)会遍历数组中的所有值并忽略函数的返回值;every(…)会一直运行到回调函数返回false;some(…)会一直运行到回调函数返回true

使用for…in遍历对象是无法直接获取属性值的,因为它实际上遍历的是对象中所有的可枚举属性,需要手动获取属性值

如果我们想直接遍历数组1的值,我们可以使用for…of循环语法

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

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

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

但实际上,@@iterator本身并不是一个迭代器对象,而是一个返回迭代器对象的函数done是一个布尔值,表示是否还有可以遍历的属性。

和数组不同,普通的对象没有内置的@@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);
}
// 2
// 3

5、总结

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()方法来遍历数据值。

你可能感兴趣的:(javascript,前端,开发语言)