3月阅读周·你不知道的JavaScript | 无处不在且重要的对象
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读两个月。
《你不知道的JavaScript》分上中下三卷,内容相对较多。3月份,我计划先读前面两卷。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》。
当前阅读周书籍:《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》。
对象
语法
对象可以通过两种形式定义:声明(文字)形式和构造形式。
声明(文字)形式的方式:
var myObj = {
key: value,
// ...
};
构造形式的方式:
var myObj = new Object();
myObj.key = value;
构造形式和文字形式生成的对象是一样的。唯一的区别是,在文字声明中你可以添加多个键/值对,但是在构造形式中你必须逐个添加属性。
类型
对象是JavaScript的基础。在JavaScript中一共有六种主要类型(术语是“语言类型”):
string、number、boolean、null、undefined、object。
JavaScript中有许多特殊的对象子类型,我们可以称之为复杂基本类型。函数就是对象的一个子类型(从技术角度来说就是“可调用的对象”)。数组也是对象的一种类型,具备一些额外的行为。数组中内容的组织方式比一般的对象要稍微复杂一些。
JavaScript中还有一些对象子类型,通常被称为内置对象:
String、Number、Boolean、Object、Function、Array、Date、RegExp、Error。
可以直接在字符串字面量上访问属性或者方法,之所以可以这样做,是因为引擎自动把字面量转换成String对象,所以可以访问属性和方法。比如下面的例子:
var strPrimitive = "I am a string";
console.log(strPrimitive.length); // 13
console.log(strPrimitive.charAt(3)); // "m"
null和undefined没有对应的构造形式,它们只有文字形式。相反,Date只有构造,没有文字形式。对于Object、Array、Function和RegExp(正则表达式)来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。
Error对象很少在代码中显式创建,一般是在抛出异常时被自动创建。也可以使用new Error(..)这种构造形式来创建,不过一般来说用不着。
内容
对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性。
var myObject = {
a: 2,
};
myObject.a; // 2
myObject['a']; // 2
上面的代码中展示了两种语法,操作符或者[]操作符,用来访问myObject中a位置上的值。
- .a语法通常被称为“属性访问”;
- ["a"]语法通常被称为“键访问”。
这两种语法的主要区别在于.操作符要求属性名满足标识符的命名规范,而[".."]语法可以接受任意UTF-8/Unicode字符串作为属性名。
可计算属性名
ES6增加了可计算属性名,可以在文字形式中使用[]包裹一个表达式来当作属性名:
var prefix = 'foo';
var myObject = {
[prefix + 'bar']: 'hello',
[prefix + 'baz']: 'world',
};
const foobar = myObject['foobar'];
const foobaz = myObject['foobaz'];
console.log(foobar); // hello
console.log(foobaz); // world
属性与方法
无论返回值是什么类型,每次访问对象的属性就是属性访问。如果属性访问返回的是一个函数,那它也并不是一个“方法”。属性访问返回的函数和其他函数没有任何区别(除了可能发生的隐式绑定this,就像我们刚才提到的)。
function foo() {
console.log('foo');
}
var someFoo = foo; // 对foo的变量引用
var myObject = {
someFoo: foo,
};
foo; // function foo(){..}
someFoo; // function foo(){..}
myObject.someFoo; // foo(){..}
在上面的代码中,someFoo和myObject.someFoo只是对于同一个函数的不同引用,并不能说明这个函数是特别的或者“属于”某个对象。如果foo()定义时在内部有一个this引用,那这两个函数引用的唯一区别就是myObject.someFoo中的this会被隐式绑定到一个对象。无论哪种引用形式都不能称之为“方法”。
数组
数组也支持[]访问形式。此外,数组有一套更加结构化的值存储机制。数组期望的是数值下标,也就是说值存储的位置(通常被称为索引)是非负整数,比如说0和42:
var myArray = ['foo', 42, 'bar'];
myArray.length; // 3
myArray[0]; // "foo"
myArray[2]; // "bar"
复制对象
有一种巧妙的复制对象的方法:
var newObj = JSON.parse(JSON.stringify(someObj));
这种方法需要保证对象是JSON安全的,所以只适用于部分情况。
ES6定义了Object.assign(..)方法来实现浅复制。Object.assign(..)方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举(enumerable,参见下面的代码)的自有键(owned key,很快会介绍)并把它们复制(使用=操作符赋值)到目标对象,最后返回目标对象,就像这样:
var newObj = Object.assign({}, myObject);
newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true
属性描述符
从ES5开始,所有的属性都具备了属性描述符。可以使用Object.getOwnPropertyDescriptor查看对象的属性描述符:
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
不变性
所有的方法创建的都是浅不变性,也就是说,它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数,等),其他对象的内容不受影响,仍然是可变的:
myImmutableObject.foo; // [1,2,3]
myImmutableObject.foo.push(4);
myImmutableObject.foo; // [1,2,3,4]
[[Get]]
在语言规范中,myObject.a在myObject上实际上是实现了[[Get]]操作(有点像函数调用:[[Get]]())。对象默认的内置[[Get]]操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。
如果无论如何都没有找到名称相同的属性,那[[Get]]操作会返回值undefined:
var myObject = {
a: 2,
};
myObject.b; // undefined
[[Put]]
如果已经存在这个属性,[[Put]]算法大致会检查下面这些内容。
- 属性是否是访问描述符(参见3.3.9节)?如果是并且存在setter就调用setter。
- 属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,在严格模式下抛出TypeError异常。
- 如果都不是,将该值设置为属性的值。
如果对象中不存在这个属性,[[Put]]操作会更加复杂。
Getter和Setter
在ES5中可以使用getter和setter部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。
存在性
可以在不访问属性值的情况下判断对象中是否存在这个属性:
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上。
遍历
1、for..in循环可以用来遍历对象的可枚举属性列表(包括[[Prototype]]链)。但是无法直接获取属性值的,因为它实际上遍历的是对象中的所有可枚举属性,你需要手动获取属性值。
2、对于数值索引的数组来说,可以使用标准的for循环来遍历值:
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
console.log(myArray[i]);
}
// 1 2 3
3、forEach(..)会遍历数组中的所有值并忽略回调函数的返回值。
4、every(..)会一直运行直到回调函数返回false(或者“假”值), some(..)会一直运行直到回调函数返回true(或者“真”值)。every(..)和some(..)中特殊的返回值和普通for循环中的break语句类似,它们会提前终止遍历。
5、ES6增加了一种用来遍历数组的for..of循环语法(如果对象本身定义了迭代器的话也可以遍历对象):
var myArray = [1, 2, 3];
for (var v of myArray) {
console.log(v);
}
打印一下结果:
for..of循环每次调用myObject迭代器对象的next()方法时,内部的指针都会向前移动并返回对象属性列表的下一个值(再次提醒,需要注意遍历对象属性/值时的顺序)。
总结
我们来总结一下本篇的主要内容:
- JavaScript中的对象有字面形式(比如var a = { .. })和构造形式(比如var a =new Array(..))。字面形式更常用,不过有时候构造形式可以提供更多选项。
- 对象是6个(或者是7个,取决于你的观点)基础类型之一。对象有包括function在内的子类型,不同子类型具有不同的行为,比如内部标签[object Array]表示这是对象的子类型数组。
- 对象就是键/值对的集合。可以通过.propName或者["propName"]语法来获取属性值。访问属性时,引擎实际上会调用内部的默认[[Get]]操作(在设置属性值时是[[Put]]), [[Get]]操作会检查对象本身是否包含这个属性,如果没找到的话还会查找[[Prototype]]链。
- 属性的特性可以通过属性描述符来控制,比如writable和configurable。此外,可以使用Object.preventExtensions(..)、Object.seal(..)和Object.freeze(..)来设置对象(及其属性)的不可变性级别。
- 可以使用ES6的for..of语法来遍历数据结构(数组、对象,等等)中的值,for..of会寻找内置或者自定义的@@iterator对象并调用它的next()方法来遍历数据值。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)