3月阅读周·你不知道的JavaScript | 无处不在且重要的对象

举报
叶一一 发表于 2024/03/25 09:51:20 2024/03/25
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读两个月。《你不知道的JavaScript》分上中下三卷,内容相对较多。3月份,我计划先读前面两卷。已读完书籍:《架构简洁之道》、《深入浅出的...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出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);
}

打印一下结果:

3-1.png

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畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。