《JavaScript 闯关记》之对象

对象是 JavaScript 的数据类型。它将不少值(原始值或者其余对象)聚合在一块儿,可经过名字访问这些值,所以咱们能够把它当作是从字符串到值的映射。对象是动态的,能够随时新增和删除自有属性。对象除了能够保持自有的属性,还能够从一个称为原型的对象继承属性,这种「原型式继承(prototypal inheritance)」是 JavaScript 的核心特征。javascript

对象最多见的用法是建立(create)、设置(set)、查找(query)、删除(delete)、检测(test)和枚举(enumerate)它的属性。java

属性包括名字和值。属性名能够是包含空字符串在内的任意字符串,但对象中不能存在两个同名的属性。值能够是任意 JavaScript 值,或者在 ECMAScript 5中能够是 gettersetter 函数。git

除了名字和值以外,每一个属性还有一些与之相关的值,称为「属性特性(property attribute)」:github

  • 可写(writable attribute),代表是否能够设置该属性的值。
  • 可枚举(enumerable attribute),代表是否能够经过 for-in 循环返回该属性。
  • 可配置(configurable attribute),代表是否能够删除或修改该属性。

在 ECMAScript 5以前,经过代码给对象建立的全部属性都是可写的、可枚举的和可配置的。在 ECMAScript 5中则能够对这些特性加以配置。正则表达式

除了包含属性特性以外,每一个对象还拥有三个相关的「对象特性(object attribute)」:json

  • 对象的类(class),是一个标识对象类型的字符串。
  • 对象的原型(prototype),指向另一个对象,本对象的属性继承自它的原型对象。
  • 对象的扩展标记(extensible flag),指明了在 ECMAScript 5中是否能够向该对象添加新属性。

最后,用下面术语来对 JavaScript 的「三类对象」和「两类属性」进行区分:数组

  • 内置对象(native object),是由 JavaScript 规范定义的对象或类。例如,数组、函数、日期和正则表达式都是内置对象。
  • 宿主对象(host object),是由 JavaScript 解释器所嵌入的宿主环境(好比 Web 浏览器)定义的。客户端 JavaScript 中表示网页结构的 HTMLElement 对象均是宿主对象。
  • 自定义对象(user-defined object),是由运行中的 JavaScript 代码建立的对象。
  • 自有属性(own property),是直接在对象中定义的属性。
  • 继承属性(inherited property),是在对象的原型对象中定义的属性。

建立对象

可使用对象字面量、new 关键字和 ECMAScript 5中的 Object.create() 函数来建立对象。浏览器

使用对象字面量建立对象(推荐)

建立对象最简单的方式就是在 JavaScript 代码中使用对象字面量。对象字面量是由若干名值对组成的映射表,名值对中间用冒号分隔,名值对之间用逗号分隔,整个映射表用花括号括起来。属性名能够是 JavaScript 标识符也能够是字符串直接量(包括空字符串)。属性的值能够是任意类型的 JavaScript 表达式,表达式的值(能够是原始值也能够是对象值)就是这个属性的值。例如:微信

// 推荐写法
var person = {
    name : "stone",
    age : 28
};

// 也能够写成
var person = {};
person.name = "stone";
person.age = 28;复制代码

使用 new 关键字建立对象

new 关键字建立并初始化一个新对象。关键字 new 后跟随一个函数调用。这里的函数称作构造函数(constructor),构造函数用以初始化一个新建立的对象。JavaScript 语言核心中的原始类型都包含内置构造函数。例如:less

var person = new Object();
person.name = "stone";
person.age = 28;复制代码

其中 var person = new Object(); 等价于 var person = {};

使用 Object.create() 函数建立对象

ECMAScript 5定义了一个名为 Object.create() 的方法,它建立一个新对象,其中第一个参数是这个对象的原型。Object.create() 提供第二个可选参数,用以对对象的属性进行进一步描述。Object.create() 是一个静态函数,而不是提供给某个对象调用的方法。使用它的方法很简单,只须传入所需的原型对象便可。例如:

var person = Object.create(Object.prototype);
person.name = "stone";
person.age = 28;复制代码

其中 var person = Object.create(Object.prototype); 也等价于 var person = {};

原型(prototype)

全部经过对象字面量建立的对象都具备同一个原型对象,并能够经过 JavaScript 代码 Object.prototype 得到对原型对象的引用。经过关键字 new 和构造函数调用建立的对象的原型就是构造函数的 prototype 属性的值。所以,同使用 {} 建立对象同样,经过 new Object() 建立的对象也继承自 Object.prototype。一样,经过 new Array() 建立的对象的原型就是 Array.prototype,经过 new Date() 建立的对象的原型就是 Date.prototype

没有原型的对象为数很少,Object.prototype 就是其中之一。它不继承任何属性。其余原型对象都是普通对象,普通对象都具备原型。全部的内置构造函数(以及大部分自定义的构造函数)都具备一个继承自 Object.prototype 的原型。例如,Date.prototype 的属性继承自 Object.prototype,所以由 new Date() 建立的 Date 对象的属性同时继承自 Date.prototypeObject.prototype

这一系列连接的原型对象就是所谓的「原型链(prototype chain)」。

属性的查询和设置

前面有提到过,能够经过点 . 或方括号 [] 运算符来获取属性的值。对于点 . 来讲,左侧应当是一个对象,右侧必须是一个以属性名称命名的简单标识符。对于方括号来讲 [] ,方括号内必须是一个计算结果为字符串的表达式,这个字符串就是属性的名称。例如:

// 推荐写法
console.log(person.name);   // "stone"
console.log(person.age);    // "28"

// 也能够写成
console.log(person["name"]);    // stone
console.log(person["age"]);     // 28复制代码

和获取属性的值写法同样,经过点和方括号也能够建立属性或给属性赋值,但须要将它们放在赋值表达式的左侧。例如:

// 推荐写法
person.name = "sophie"; // 赋值
person.age = 30;        // 赋值
person.weight = 38;     // 建立

// 也能够写成
person["name"] = "sophie";  // 赋值
person["age"] = 30;         // 赋值
person["weight"] = 38;      // 建立复制代码

当使用方括号时,方括号内的表达式必须返回字符串。更严格地讲,表达式必须返回字符串或返回一个能够转换为字符串的值。

属性的访问错误

查询一个不存在的属性并不会报错,若是在对象 o 自身的属性或继承的属性中均未找到属性 x,属性访问表达式 o.x 返回 undefined。例如:

var person = {};
person.wife;    // undefined复制代码

可是,若是对象不存在,那么试图查询这个不存在的对象的属性就会报错。nullundefined 值都没有属性,所以查询这些值的属性会报错。例如:

var person = {};
person.wife.name;   // Uncaught TypeError: Cannot read property 'name' of undefined.复制代码

除非肯定 personperson.wife 都是对象,不然不能这样写表达式 person.wife.name,由于会报「未捕获的错误类型」,下面提供了两种避免出错的方法:

// 冗余但易懂的写法
var name;
if (person) {
    if (person.wife) 
        name = person.wife.name;
}

// 简练又经常使用的写法(推荐写法)
var name = person && person.wife && person.wife.name;复制代码

删除属性

delete 运算符用来删除对象属性,事实上 delete 只是断开属性和宿主对象的联系,并无真正的删除它。delete 运算符只能删除自有属性,不能删除继承属性(要删除继承属性必须从定义这个属性的原型对象上删除它,并且这会影响到全部继承自这个原型的对象)。

代码范例,请参见「变量和数据类型」-「数据类型」-「delete 运算符」

检测属性

JavaScript 对象能够看作属性的集合,咱们常常会检测集合中成员的所属关系(判断某个属性是否存在于某个对象中)。能够经过 in 运算符、hasOwnPreperty()propertyIsEnumerable() 来完成这个工做,甚至仅经过属性查询也能够作到这一点。

in 运算符的左侧是属性名(字符串),右侧是对象。若是对象的自有属性或继承属性中包含这个属性则返回 true。例如:

var o = { x: 1 }
console.log("x" in o);          // true,x是o的属性
console.log("y" in o);          // false,y不是o的属性
console.log("toString" in o);   // true,toString是继承属性复制代码

对象的 hasOwnProperty() 方法用来检测给定的名字是不是对象的自有属性。对于继承属性它将返回 false。例如:

var o = { x: 1 }
console.log(o.hasOwnProperty("x"));          // true,x是o的自有属性
console.log(o.hasOwnProperty("y"));          // false,y不是o的属性
console.log(o.hasOwnProperty("toString"));   // false,toString是继承属性复制代码

propertyIsEnumerable()hasOwnProperty() 的加强版,只有检测到是自有属性且这个属性的可枚举性(enumerable attribute)为 true 时它才返回 true。某些内置属性是不可枚举的。一般由 JavaScript 代码建立的属性都是可枚举的,除非在 ECMAScript 5中使用一个特殊的方法来改变属性的可枚举性。例如:

var o = inherit({ y: 2 });
o.x = 1;
o.propertyIsEnumerable("x");    // true:,x是o的自有属性,可枚举
o.propertyIsEnumerable("y");    // false,y是继承属性
Object.prototype.propertyIsEnumerable("toString");  // false,不可枚举复制代码

除了使用 in 运算符以外,另外一种更简便的方法是使用 !== 判断一个属性是不是 undefined。例如:

var o = { x: 1 }
console.log(o.x !== undefined);              // true,x是o的属性
console.log(o.y !== undefined);              // false,y不是o的属性
console.log(o.toString !== undefined);       // true,toString是继承属性复制代码

然而有一种场景只能使用 in 运算符而不能使用上述属性访问的方式。in 能够区分不存在的属性和存在但值为 undefined 的属性。例如:

var o = { x: undefined }        // 属性被显式赋值为undefined
console.log(o.x !== undefined); // false,属性存在,但值为undefined
console.log(o.y !== undefined); // false,属性不存在
console.log("x" in o);          // true,属性存在
console.log("y" in o);          // false,属性不存在
console.log(delete o.x);        // true,删除了属性x
console.log("x" in o);          // false,属性再也不存在复制代码

扩展阅读「JavaScript 检测原始值、引用值、属性」
shijiajie.com/2016/06/20/…

扩展阅读「JavaScript 检测之 basevalidate.js」
shijiajie.com/2016/06/25/…

枚举属性

除了检测对象的属性是否存在,咱们还会常常遍历对象的属性。一般使用 for-in 循环遍历,ECMAScript 5提供了两个更好用的替代方案。

for-in 循环能够在循环体中遍历对象中全部可枚举的属性(包括自有属性和继承的属性),把属性名称赋值给循环变量。对象继承的内置方法不可枚举的,但在代码中给对象添加的属性都是可枚举的。例如:

var o = {x:1, y:2, z:3};            // 三个可枚举的自有属性
o.propertyIsEnumerable("toString"); // false,不可枚举
for (p in o) {          // 遍历属性
    console.log(p);     // 输出x、y和z,不会输出toString
}复制代码

有许多实用工具库给 Object.prototype 添加了新的方法或属性,这些方法和属性能够被全部对象继承并使用。然而在ECMAScript 5标准以前,这些新添加的方法是不能定义为不可枚举的,所以它们均可以在 for-in 循环中枚举出来。为了不这种状况,须要过滤 for-in 循环返回的属性,下面两种方式是最多见的:

for(p in o) {
   if (!o.hasOwnProperty(p)) continue;          // 跳过继承的属性
   if (typeof o[p] === "function") continue;    // 跳过方法
}复制代码

除了 for-in 循环以外,ECMAScript 5定义了两个用以枚举属性名称的函数。第一个是 Object.keys(),它返回一个数组,这个数组由对象中可枚举的自有属性的名称组成。第二个是 Object.getOwnPropertyNames(),它和 Ojbect.keys() 相似,只是它返回对象的全部自有属性的名称,而不只仅是可枚举的属性。在ECMAScript 3中是没法实现的相似的函数的,由于ECMAScript 3中没有提供任何方法来获取对象不可枚举的属性。

属性的 gettersetter

咱们知道,对象属性是由名字、值和一组特性(attribute)构成的。在ECMAScript 5中,属性值能够用一个或两个方法替代,这两个方法就是 gettersetter。由 gettersetter 定义的属性称作「存取器属性(accessor property)」,它不一样于「数据属性(data property)」,数据属性只有一个简单的值。

当程序查询存取器属性的值时,JavaScript 调用 getter 方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript 调用 setter 方法,将赋值表达式右侧的值当作参数传入 setter。从某种意义上讲,这个方法负责「设置」属性值。能够忽略 setter 方法的返回值。

和数据属性不一样,存取器属性不具备可写性(writable attribute)。若是属性同时具备 gettersetter 方法,那么它是一个读/写属性。若是它只有 getter 方法,那么它是一个只读属性。若是它只有 setter 方法,那么它是一个只写属性,读取只写属性老是返回 undefined。定义存取器属性最简单的方法是使用对象直接量语法的一种扩展写法。例如:

var o = {
    // 普通的数据属性
    data_prop: value,

    // 存取器属性都是成对定义的函数
    get accessor_prop() { /*这里是函数体 */ },
    set accessor_prop(value) { /* 这里是函数体*/ }
};复制代码

存取器属性定义为一个或两个和属性同名的函数,这个函数定义没有使用 function 关键字,而是使用 getset。注意,这里没有使用冒号将属性名和函数体分隔开,但在函数体的结束和下一个方法或数据属性之间有逗号分隔。

序列化对象(JSON)

对象序列化(serialization)是指将对象的状态转换为字符串,也可将字符串还原为对象。ECMAScript 5提供了内置函数 JSON.stringify()JSON.parse() 用来序列化和还原 JavaScript 对象。这些方法都使用 JSON 做为数据交换格式,JSON 的全称是「JavaScript 对象表示法(JavaScript Object Notation)」,它的语法和 JavaScript 对象与数组直接量的语法很是相近。例如:

o = {x:1, y:{z:[false,null,""]}};       // 定义一个对象
s = JSON.stringify(o);                  // s是 '{"x":1,"y":{"z":[false,null,""]}}'
p = JSON.parse(s);                      // p是o的深拷贝复制代码

ECMAScript 5中的这些函数的本地实现和 github.com/douglascroc… 中的公共域ECMAScript 3版本的实现很是相似,或者说彻底同样,所以能够经过引入 json2.js 模块在ECMAScript 3的环境中使用ECMAScript 5中的这些函数。

JSON 的语法是 JavaScript 语法的子集,它并不能表示 JavaScript 里的全部值。它支持对象、数组、字符串、无穷大数字、truefalsenull,能够序列化和还原它们。NaNInfinity-Infinity 序列化的结果是 null,日期对象序列化的结果是 ISO 格式的日期字符串(参照 Date.toJSON() 函数),但 JSON.parse() 依然保留它们的字符串形态,而不会将它们还原为原始日期对象。函数、RegExpError 对象和 undefined 值不能序列化和还原。JSON.stringify() 只能序列化对象可枚举的自有属性。对于一个不能序列化的属性来讲,在序列化后的输出字符串中会将这个属性省略掉。JSON.stringify()JSON.parse() 均可以接收第二个可选参数,经过传入须要序列化或还原的属性列表来定制自定义的序列化或还原操做。

关卡

请实现下面用来枚举属性的对象工具函数:

/* * 把 p 中的可枚举属性复制到 o 中,并返回 o * 若是 o 和 p 中含有同名属性,则覆盖 o 中的属性 */
function extend(o, p) {
    // 请实现函数体
}复制代码
/* * 将 p 中的可枚举属性复制至 o 中,并返回 o * 若是 o 和 p 中有同名的属性,o 中的属性将不受影响 */
function merge(o, p) {
    // 请实现函数体
}复制代码
/* * 若是 o 中的属性在 p 中没有同名属性,则从 o 中删除这个属性 * 返回 o */
function restrict(o, p) {
    // 请实现函数体
}复制代码
/* * 若是 o 中的属性在 p 中存在同名属性,则从 o 中删除这个属性 * 返回 o */
function subtract(o, p) {
    // 请实现函数体
}复制代码
/* * 返回一个新对象,这个对象同时拥有 o 的属性和 p 的属性 * 若是 o 和 p 中有重名属性,使用 p 中的属性值 */
function union(o, p) { 
    // 请实现函数体
}复制代码
/* * 返回一个新对象,这个对象拥有同时在 o 和 p 中出现的属性 * 很像求 o 和 p 的交集,但 p 中属性的值被忽略 */
function intersection(o, p) { 
    // 请实现函数体
}复制代码
/* * 返回一个数组,这个数组包含的是 o 中可枚举的自有属性的名字 */
function keys(o) {
    // 请实现函数体
}复制代码

更多

关注微信公众号「劼哥舍」回复「答案」,获取关卡详解。
关注 github.com/stone0090/j…,获取最新动态。

相关文章
相关标签/搜索