• 欢迎访问天天编码网站,Java技术、技术书单、开发工具,欢迎加入天天编码
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏天天编码吧
  • 我们的淘宝店铺已经开张了哦,传送门:https://shop145764801.taobao.com/

4.1 Object

JS 教程 tiantian 119次浏览 0个评论 扫描二维码

从本教程的数据类型 章节可知,JavaScript 中存在7种数据类型。其中的6种被称为“原始类型”,因为它们的值只包含一个单一的东西(可能是一个字符串或者一个数值或者其他)。

作为对比,object 被用来存储按照键值排练的多种数据和更加复杂的实体。在 JavaScript 中,object 几乎穿透了语言的每一个方面。所以,在我们继续深入学习JavaScript的其他特性之前,我们先要学习 object。

可以直接使用大括号{...}和一系列可选的 属性(properties) 来创建一个 object。一个 property 是一个“key: value”键值对,其中的 key是一个字符串(又被称为一个”property name”),其中的value可以是任何值。

我们可以将 object 想象策划那个一个装满标记文件的文件夹。所有的数据被存储在对应的文件中,并被 key 标记。通过它的名称,可以非常容易地实现查询文件、添加或者移除文件。

4.1 Object

一个空的 object(空的文件夹)可以使用如下的两个语法来创建:

let user = new Object(); // "object constructor" syntax
let user = {};  // "object literal" syntax

4.1 Object

一般地,通常使用大括号语法{...}。那个定义又被称为 object literal。

属性(property)

在利用{...}创建对象时,我们可以立即往对象中添加“key:value”的键值对:

let user = {     // an object
  name: "John",  // by key "name" store value "John"
  age: 30        // by key "age" store value 30
};

一个属性具有一个键(又被称为一个”名称“或者”识别码“),位于冒号“:”之前,后面再接一个相应的值。

在上述的userobject中,存在两个属性:

  1. 第一个属性的名称是“name”,它的值是"John"
  2. 第一个属性的名称是"age",它的值是"30"

那个最终的userobject 可以被想象成一个具有两个识别码,分别是“name”和“age”,文件的文件夹。

4.1 Object

我们可以在任何时间往对象里添加、移除和阅读文件。

利用 dot .符号可以访问对象的属性:

// get fields of the object:
alert( user.name ); // John
alert( user.age ); // 30

属性值可以是任何类型。我们往对象中添加一个 boolean 值:

user.isAdmin = true;

4.1 Object

如果想要移除对象中的属性,我们可以使用delete操作符:

delete user.age;

4.1 Object

实际上,我们也可以使用含有多个单词的属性名,单词之间使用空格分隔,这种情况下我们必须使用引号:

let user = {
  name: "John",
  age: 30,
  "likes birds": true  // multiword property name must be quoted
};

4.1 Object

对象的最后一个属性的后面可能会有一个逗号:

let user = {
  name: "John",
  age: 30,
}

这被称为一个“尾部”或者“悬挂”的逗号。这个逗号为对象增加、移除、移动属性增加了很大地方便性,因为所有的属性行变成了一致的格式。

方括号

对于多单词的属性,那个 dot.访问符无法正常工作:

// this would give a syntax error
user.likes birds = true

这是因为 dot. 要求后接的键必须是一个合法的变量识别码。这意味着:该键值不能含有空白符或者其他的分隔符。

所以,JavaScript 中存在一种被称为“方括号符号”的属性访问方式,可以处理任何的字符串键值:

let user = {};

// set
user["likes birds"] = true;

// get
alert(user["likes birds"]); // true

// delete
delete user["likes birds"];

现在,上述代码工作正常。请注意:位于方括号内的字符串应该被正确地引号包围,任何类型的引号都可以。

此外,方括号还提供了一种获取任何表达式的结果作为属性名的方式,而不仅仅是一个字符串字面量。比如,利用一个变量来作为属性的键值:

let key = "likes birds";

// same as user["likes birds"] = true;
user[key] = true;

此处,那个变量key可能是在运行时才被计算获得结果,或者依赖于用户的输入。然后,我们使用该变量来访问对象的对应属性。这种方式给予我们操作对象属性方面很多的便利性。而常见的 dot . 方式不能使用类似的方式。

举个例子:

let user = {
  name: "John",
  age: 30
};

let key = prompt("What do you want to know about the user?", "name");

// access by variable
alert( user[key] ); // John (if enter "name")

计算属性

我们可以直接在 object literal 中使用方括号来定义属性。那种属性被称为 computed properties(计算属性)。

举个例子:

let fruit = prompt("Which fruit to buy?", "apple");

let bag = {
  [fruit]: 5, // the name of the property is taken from the variable fruit
};

alert( bag.apple ); // 5 if fruit="apple"

计算属性的概念是非常简单的:[fruit] 意味着这个属性的键应该来自于变量fruit的值。

所以,如果用户输入"apple"bag就变成了{apple: 5}

本质上,上述的示例代码与如下代码功能一致:

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};

// take property name from the fruit variable
bag[fruit] = 5;

但是,上述的代码看起来更加漂亮一些。

实际上,我们可以在方括号中使用更加复杂的表达式:

let fruit = 'apple';
let bag = {
  [fruit + 'Computers']: 5 // bag.appleComputers = 5
};

可以发现,方括号的功能比dot.符号的功能强大很多。它允许任意的属性名和变量做为属性名。但是,它们在书写上更加费力一些。

所以,在绝大多数情况下,特别是属性名称已知或者非常简单的情况下,建议使用 dot.符号。但是,如果我们需要某些特殊或复杂的属性名称,就可以切换到方括号方式。

保留字可以作为属性名

我们知道,一个变量的名称不能相同与语言的任意保留字,比如“for”,“let”,“return”等等。

但是,对于对象属性而言,不存在这样的限制。任何名称都是合法的:

let obj = {
 for: 1,
 let: 2,
 return: 3
}

alert( obj.for + obj.let + obj.return );  // 6

基本上,任何名称都是合法的,但是存在一个特殊的情况:"__proto__"因为历史原因,被JavaScript引擎特殊对待。举个例子,我们不能为其设置非对象的值:

let obj = {};
obj.__proto__ = 5;
alert(obj.__proto__); // [object Object], didn't work as intended

正如我们所见,为该属性赋值的原始值5直接被忽略了。

这可能会导致出现源码级别的 bug 或者代码功能错误,如果我们期望对象可以存储任意的键值对,并且允许用户指定任意的键值。

在某些情况下,用户可能选择的键值正是上述的__proto__,然后那个赋值操作就会直接被引擎忽略。

存在一种方法使得对象把__proto__当作普通的属性来对待,我们将在后面讲解该方法,我们先要学习更多有关 object 的知识。存在另外一种数据结构名为Map,我们将在Map,Set,WeakMap和WeakSet章节对它进行详解,它支持任意类型做键值。

属性值

在实际的编程实践中,我们经常使用已经存在的变量作用同名属性的属性值

比如:

function makeUser(name, age) {
  return {
    name: name,
    age: age
    // ...other properties
  };
}

let user = makeUser("John", 30);
alert(user.name); // John

在上述示例代码中,属性名与变量的名称相同。这种利用变量作为同名属性的值的情况是如此地普通,所以存在一种特殊的属性值速记语法来简化代码编写。

不使用常见的 name:name,我们可以直接写成name,就像下面:

function makeUser(name, age) {
  return {
    name, // same as name: name
    age   // same as age: age
    // ...
  };
}

当然,我们也可以在同一个对象中混合使用正常属性名和速写属性名:

let user = {
  name,  // same as name:name
  age: 30
};

存在性检测

object 的一个重要特性是可以访问对象的任何特性。即使在没有对应属性存在的情况下也不会产生出任何错误。访问一个不存在的属性只会返回undefined。这提供了一种检测某个属性是否存在的通用方法——获取该属性并与undefined进行比较:

let user = {};

alert( user.noSuchProperty === undefined ); // true means "no such property"

此外,还存在一种特殊的操作符"in",可用来检测对象属性的存在性。

操作符的语法是:

"key" in object

举个例子:

let user = { name: "John", age: 30 };

alert( "age" in user ); // true, user.age exists
alert( "blabla" in user ); // false, user.blabla doesn't exist

请注意,in操作的左边必须是一个属性名。一般而言,就是一个被引号包裹的字符串。

如果我们省略了引号,这意味着是一个变量,其中包括了被用来测试的实际名称。举个例子:

let user = { age: 30 };

let key = "age";
alert( key in user ); // true, takes the name from key and checks for such property

使用”in”处理值为undefined的属性

一般地,那个严格的相等性测试"=== undefined" 可以正常实现检测。但是在某些特殊的情况下该测试会失效,但是in关键值可以正常工作。

这个特殊情形就是对象的属性存在,但是属性的值为undefined

let obj = {
 test: undefined
};

alert( obj.test ); // it's undefined, so - no such property?

alert( "test" in obj ); // true, the property does exist!

在上述代码中,属性obj.test在语法上是存在的。所以in操作符工作正常。

当然,想这样的状况很少出现,一般情况下,不会给变量赋值为undefined。我们通常使用null来表示”未知“或者”空“的值。所以上述的in操作符并没有想象中那么实用。

那个 “for…in” 循环

为了遍历一个对象的所有键值,存在一个特殊的类型的循环:for..in。这和我们以前学过的for(;;)循环结构是完全不同的概念。

该循环的语法如下:

for(key in object) {
  // executes the body for each key among object properties
}

举个例子,我们来输出user对象的所有属性:

let user = {
  name: "John",
  age: 30,
  isAdmin: true
};

for(let key in user) {
  // keys
  alert( key );  // name, age, isAdmin
  // values for the keys
  alert( user[key] ); // John, 30, true
}

请注意:所有的”for”循环结构都允许我们在循环的内部来定义循环变量,比如此处的let key

当然,我们也可以使用另外的变量名,而不一定是key。比如"for(let prop in obj)" 也是非常常见的使用惯例。

像对象一样排序

对象是有序的吗?换句话说,如果我们迭代一个对象,我们是否以它们被添加的顺序来获取那些属性?我们可以依赖这个规则吗?

简单回答是:”以一种特殊的方式有序“:整形属性按照从小到大排序,其他属性保持它们的创建时顺序。详细的细节如下:

作为一个示例,我们来考虑一个存储电话号码的对象:

let codes = {
  "49": "Germany",
  "41": "Switzerland",
  "44": "Great Britain",
  // ..,
  "1": "USA"
};

for(let code in codes) {
  alert(code); // 1, 41, 44, 49
}

这个对象可以用来给用户提供电话号码的选项列表。如果我们的产品,比如一个网站,主要的用户都是德国区域的话,我们可能希望49可以排在这个选项列表的最前面。

但是,如果我们运行该代码,可能会发现一个完全不同的画面:

  • USA(1) 排在最前面
  • 然后就是 Switzerland(41) 等等。

那些电话号码按照降序的方式排列,因为它们是整形数值。所以我们看到了1, 41, 44, 49

什么是整形属性?

整形属性的含义是指可以转成成整形,而且再转换成字符串,都不会导致该属性发生任何变化的属性。

所以,”49″就是一个整数属性的名称,当它被转换成一个整数数字,并且被转换回来的时候,它完全等于原始值。但是”+49“和”1.2“等就不是整形属性了:

// Math.trunc is a built-in function that removes the decimal part
alert( String(Math.trunc(Number("49"))) ); // "49", same, integer property
alert( String(Math.trunc(Number("+49"))) ); // "49", not same "+49"  not integer property
alert( String(Math.trunc(Number("1.2"))) ); // "1", not same "1.2"  not integer property

另一方面,如果属性不是整形属性,那么它们就会保持它们被创建时的顺序,举个例子:

let user = {
  name: "John",
  surname: "Smith"
};
user.age = 25; // add one more

// non-integer properties are listed in the creation order
for (let prop in user) {
  alert( prop ); // name, surname, age
}

所以,为了修正上述示例中的那个电话号码问题,我们可以使用一点小技巧来欺骗执行引擎。比如,添加一个多余的"+"符号到每一个属性代码的前面。

比如:

let codes = {
  "+49": "Germany",
  "+41": "Switzerland",
  "+44": "Great Britain",
  // ..,
  "+1": "USA"
};

for(let code in codes) {
  alert( +code ); // 49, 41, 44, 1
}

现在,这个代码正如我们所希望的那样工作。

引用复制

JavaScript 中的原始类型与object(对象)类型之间最大的一个区别就在于:对象类型在存储和复制时都是引用。

原始类型:字符串、数字、布尔值——都是直接操作和复制的整个原始值。

举个例子:

let message = "Hello!";
let phrase = message;

结果就是我们将获得两个独立的变量,每一个变量都存储了字符串"Hello!"

4.1 Object

但是,对象的工作方式完全不一样。

一个变量不是存储该对象本身,而是该对象的“内存地址”,或者说对象的“一个引用”。

下面,来看看对象的操作图解:

let user = {
  name: "John"
};

4.1 Object

此处,对象存储在内存中的某个位置。然后变量user只是存储了该对象的“一个引用”。

当一个对象变量被复制时,复制的是其存储的引用,对象并不会被复制。

如果我们将对象想象成一个文件夹,那么变量就是一个访问文件夹的钥匙,复制变量只会复制该钥匙,而不是复制该文件夹本身。

举个例子:

let user = { name: "John" };

let admin = user; // copy the reference

现在,我们具有两个变量,每个变量都具有一个引用,它们指向同一个对象:

4.1 Object

我们可以使用对象的任意一个引用来访问该对象,并且修改其内容:

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // changed by the "admin" reference

alert(user.name); // 'Pete', changes are seen from the "user" reference

上述的那个示例明确地演示了此处只存在一个对象。我们具有一个文件夹,同时具有引用它的两个钥匙,我们使用其中的某个钥匙(admin)来访问并修改它。然后,如果我们使用另一个钥匙(user)来查看该对象,我们可以正确获取修改后的值。

引用比较

那个相等比较== 和严格相等比较===对于参数为对象的情况,具有完全相同的效果。

两个对象只在它们是同一个对象的情况下才相等

举个例子,两个变量引用同一个对象的情况下,它们就是相等的:

let a = {};
let b = a; // copy the reference

alert( a == b ); // true, both variables reference the same object
alert( a === b ); // true

而且,如果是两个独立的对象,即使它们具有完全一样的内容,它们也不相等:

let a = {};
let b = {}; // two independent objects

alert( a == b ); // false

对于对象的比较操作,比如obj1 > obj2, 或者将对象与原始类型进行比较,比如obj == 5,对象将被转换成原始类型来进行比较。我们很快就会学到如何将对象转换成原始类型。但是,非常坦白地说,像这样的比较操作只在很罕见的情况下才是必要的,大部分情况下都是代码设计不合理的后果。

常量对象

一个被定义为const的对象也是可以被修改的。

举个例子:

const user = {
  name: "John"
};

user.age = 25; // (*)

alert(user.age); // 25

虽然在表面看起来,(*)行所在的代码会导致一个错误,但是,这样的代码并不会导致任何错误。这是因为const只是用来修饰变量user本身。而且,此处的变量user从始至终只存储了同一个变量的引用。那个(*)操作的是对象的内部,它并没有修改user的值。

如果我们尝试修改被const修饰的那个变量user本身,那么就会导致错误,比如:

const user = {
  name: "John"
};

// Error (can't reassign user)
user = {
  name: "Pete"
};

但是,如果我们希望将对象的某个属性做成常量呢?这样之后user.age = 25就会产生一个错误。这是可能实现的。我们将在属性标记和描述符 中讲解实现该功能的方法。

克隆和合并,Object.assign

所以,复制一个对象变量的结果就是创建了一个引用该对象的新引用。

但是,如果我们希望复制某个对象本身呢?创建一个独立的复制品,一个克隆体?

这样做也是可行的,但是实现语法有点复杂而已,因为在 JavaScript 中没有内建的方法来实现该功能。实际上,该功能是很少被使用的。引用复制在绝大多数情况下都是合适的。

但是,如果确实需要这样一个复制对象本身的功能呢?那么,我们需要创建一个全新的对象,并且通过遍历和复制已存在对象的所有原始类型级别的属性,并将它们赋值给新创建的对象来完成该功能。

整个算法流程如下:

let user = {
  name: "John",
  age: 30
};

let clone = {}; // the new empty object

// let's copy all user properties into it
for (let key in user) {
  clone[key] = user[key];
}

// now clone is a fully independant clone
clone.name = "Pete"; // changed the data in it

alert( user.name ); // still John in the original object

此外,我们也可以直接利用 Object.assign 方法来完成该功能。

该方法的语法如下:

Object.assign(dest[, src1, src2, src3...])
  • 参数 dest,和src1, ..., srcN(数量可以任意多个) 都是对象。
  • 该方法复制所有对象src1, ..., srcN的所有属性到dest中。换句话说,从第二个参数开始的所有参数被复制到了第一个参数中。然后返回第一个参数dest

举个例子:我们可以使用该方法来合并多个对象到一个对象:

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// copies all properties from permissions1 and permissions2 into user
Object.assign(user, permissions1, permissions2);

// now user = { name: "John", canView: true, canEdit: true }

如果那个接受对象(user)已经存在同名的属性,该属性会被覆盖:

let user = { name: "John" };

// overwrite name, add isAdmin
Object.assign(user, { name: "Pete", isAdmin: true });

// now user = { name: "Pete", isAdmin: true }

而且,我们可以使用Object.assign来代替简单克隆的循环:

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

该代码将user的所有属性都复制到了一个空的对象中,并返回了复制后的对象。实际上,该代码与循环的功能一致,只是更加短小。

到目前为止,我们都假定user的所有属性都是原始类型的。但是,属性也可能是其他对象的引用。那么我们该如何处理这些引用属性呢?

如下代码:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182

现在,使用clone.sizes = user.sizes并不能正确地完成复制,因为那个user.sizes是一个对象,它会采用引用复制的方式。所以,cloneuser将会共享同一个sizes

演示代码如下:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true, same object

// user and clone share sizes
user.sizes.width++;       // change a property from one place
alert(clone.sizes.width); // 51, see the result from the other one

为了修正那个问题,我们应该使用克隆循环来检查user[key]的每一个值,必须在该值为一个对象的情况下,继续在该对象上重复克隆循环。这也被称为是一个“deep cloning”(深度克隆)。

业界已经存在一个标准的算法来完成对象的深度复制,可以正确地处理如上的那些情况或者更加复杂的情形,该算法称为结构化克隆算法(Structured cloning algorithm)。为了避免重复制造轮子,我们可以直接使用一个实现了该算法的 JavaScript 库,名为lodash,该方法被称为_.cloneDeep(obj)

总结

Object 类似于通常意义上的数组概念,而且增加了几种特殊的特性。

它用来存储属性(key-value对),属性是指:

  • 属性键必须是字符串或者符号(symbol),通常都是字符串。
  • 属性值可以是任意类型。

为了访问对象属性,我们可以:

  • dot.运算符:obj.property
  • 方括号符号:obj["property"]。方括号允许将变量作为属性键值,比如:obj[varWithKey]

常见的对象属性操作:

  • 移除属性:delete obj.prop
  • 检查某个给定键的属性是否存在:"key" in obj
  • 迭代某个对象:for(let key in obj) 循环。

对象的赋值和复制操作都是操作引用。换句话说,一个变量不是存储那个”对象值本身“,而是该对象值的一个”引用“(内存中的地址)。所以,赋值一个这样的变量,或者将之作为一个函数的参数时,复制的是该对象的引用,而不是该对象本身。复制引用之后的所有的操作,比如添加或者移除属性,都是作用在同一个对象上。

为了可以创建一个”真正的拷贝“(克隆),我们可以使用Object.assign 或者 _.cloneDeep(obj)

我们在本章节学习的对象又被称为”平面对象”,或者简称为Object

在 JavaScript 中还存在多种其他类型的对象:

  • Array 用来存储有序的数据集合。
  • Date 用来存储日期和时间相关的信息。
  • Error 用来存储错误相关的信息。
  • 等等。

它们具有各自特殊的属性,我们将很快学习它们。有时,开发者会说“数组类型”或者“日期类型”,但是就语法而言,JavaScript 中并不存在对应的类型,而是属于一个相同的“object”数据类型。当然,它们在“object”类型的基础上进行了多种方式的扩展。

JavaScript 中的 Object 非常强大。此处,我们仅仅学习了整个 Object 主题的一些非常浅显和基础的知识。我们将在后面的教程中继续深入学习对象的知识,并学习那些扩展的特殊对象。

任务


Hello, object

分别利用一行代码,完成下列的行为:

  1. 创建一个空对象user
  2. 添加一个名为name,值为John的属性。
  3. 添加另一个名为surname,值为Smith的属性。
  4. 修改属性name的值为Pete
  5. 移除名为name的属性。

检查空值

创建一个函数 isEmpty(obj),在该对象没有属性的情况下返回true,其他情况下返回false

函数可以实现如下功能:

let schedule = {};

alert( isEmpty(schedule) ); // true

schedule["8:30"] = "get up";

alert( isEmpty(schedule) ); // false

常量对象?

你认为可以修改一个被const修饰的对象吗?

const user = {
  name: "John"
};

// does it work?
user.name = "Pete";

累加对象属性

我们具有一个存储团队所有成员薪水的对象:

let salaries = {
  John: 100,
  Ann: 160,
  Pete: 130
}

请完成代码来累加团队的所有薪水,并将结果存储在变量sum中。如上示例的最终结果应该是390

如果salaries是一个空对象,那么最终结果应该为0


对象属性翻倍

创建一个函数multiplyNumeric(obj),将对象obj的所有数值属性乘2

举个例子:

// before the call
let menu = {
  width: 200,
  height: 300,
  title: "My menu"
};

multiplyNumeric(menu);

// after the call
menu = {
  width: 400,
  height: 600,
  title: "My menu"
};

注意:此处的multiplyNumeric函数不需要返回任何值。它应该直接修改作为参数传入的对象。

P.S. 可以使用typeof来检查属性是否为数值类型。


天天编码 , 版权所有丨本文标题:4.1 Object
转载请保留页面地址:http://www.tiantianbianma.com/object.html/
喜欢 (0)
支付宝[多谢打赏]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址