JS面向对象精要(三)_理解对象

2020-03-05  本文已影响0人  Eastboat


JS面向对象精要(一)_原始类型和引用类型
JS面向对象精要(二)_函数
JS面向对象精要(三)_理解对象
JS面向对象精要(四)_构造函数和原型对象
JS面向对象精要(五)_继承

理解对象

请记住 JavaScript 中的对象是动态的,可在代码执行的任意时刻发生改变。基于类的语言会根据类的定义锁定对象,JavaScript 对象没有这种限制。JavaScript 编程一大重点就是管理那些对象,这就是为什么理解对象如何运作是理解整个 JavaScript 的关键

3.1 定义属性

前面第一章讲过有两种创建自己的对象的方式: 使用 Object 构造函数或对象字面量形式

var person1 = {
  name:"Nicholas" // 字面量形式中,属性name上隐式调用了[[Put]]
};
var person2 = new Object();
// [[Put]]
person2.name = "Nicholas";
person1.age = "Redacted";
person2.age = "Redacted";
// [[Set]] 调用覆盖上面的原属性
person1.name = "Greg";
person2.name = "Michael";

当一个属性第一次被添加给对象时,JavaScript 在对象上调用一个名为[[Put]]的内部方法。[[Put]]方法会在对象上创建一个新节点来保存属性,就像第一次在哈希表上添加一个键一样。这个操作不仅指定了初始的值,也定义了属性的一些特征,所以上面代码中当属性 name 和 age 在每个对象上第一次被定义时,[[Put]]方法都在该对象上被调用了。当一个已有的属性被赋予一个新值时,调用的是一个名为[[Set]]的方法。该方法将属性的当前值替换为新值。上例为 name 设置第二个值时,调用了[[Set]]方法。

调用[[Put]]的结果是在对象上创建了一个自有属性。一个自有属性表明仅仅该指定的对象实例拥有该属性。该属性被直接保存在实例内,对该属性的所有操作都必须通过该对象进行。(自有属性有别于原型属性)

3.2 属性探测

由于属性可以在任何时候添加,所以有时候就有必要检查对象是否已有一个属性。JavaScript 开发新手常错误地使用以下模式探测属性是否存在。

// 错误的判断  问题在于JavaScript的类型强制会影响该模式的输出结果
if (person1.age) {
  // do something with age
}

当 if 判断中的值是一个对象、非空字符串、非零数字或 true 时,判断会评估为真;而当值是一个 null、undefined、0、false、NaN 或空字符串时评估为假,由于一个对象属性可以包含这些假值,上例代码有可能导致错误的假定。

例如,当 person1.age 为 0 时,虽然 age 属性存在,if 条件仍然无法满足。**更加可靠的判断属性是否存在的方法是使用 in 操作符,实际上 in 操作符就是在哈希表中查找一个键是否存在

var person1 = {
  name: "cc",
  age: 12,
  sayName: function() {
    console.log(this.name);
  }
};
console.log("name" in person1); // true
console.log("age" in person1); // true
console.log("title" in person1); // false
console.log("sayName" in person1); // true

然而在某些情况下,你可能希望仅当一个属性是自有属性时才检查其是否存在。in 操作符会检查自有属性和原型属性,所以你不得不选择另一条途径:所有的对象都拥有的 hasOwnProperty()方法。该方法在给定的属性存在且为自有属性时返回 true。
(这是个重大区别,在第 4 章会深入讨论)

var person1 = {
  name:"Nicholas", // name是person1的一个自有属性
  sayName:function() {
    console.log(this.name);
  }
};
console.log("name" in person1);           // true
console.log(person1.hasOwnProperty("name"));   // true

// toString()方法则是一个所有对象都具有的原型属性
console.log("toString" in person1);         // true
console.log(person1.hasOwnProperty("toString")); // false

3.3 删除属性

正如属性可以在任何时候被添加到对象上,它们也可以在任何时候被移除。但设置一个属性的值为 null 并不能从对象中彻底移除那个属性,只是调用[[Set]]将 null 值替换了该属性原来的值而已,这点你已经在本章前面见过了。你需要使用 delete 操作符来彻底移除对象的一个属性。
delete 操作符针对单个对象属性调用名为[[Delete]]的内部方法。你可以认为该操作在哈希表中移除了一个键值对。当 delete 操作符成功时,它返回 true。(某些属性无法被移除,本章后续将详细讨论。)

var person1 = {
   name:"Nicholas"
};
console.log("name" in person1);     // true
delete person1.name;         // true - not output
console.log("name" in person1);     // false

// 试图访问一个不存在的属性将返回undefined
console.log(person1.name);       // undefined

3.4 属性枚举

所有你添加的属性默认都是可枚举的,也就是说你可以用 for-in 循环遍历它们。可枚举属性的内部特征[[Enumerable]]都被设置为 true。for-in 循环会枚举一个对象所有的可枚举属性并将属性名赋给一个变量

var object = {
  age: 18,
  address: "china",
  sayAge: function() {
    console.log(this.age);
  }
};

for (var property in object) {
  console.log(`Name: ${property}`);
  console.log(`Value: ${object[property]}`);
}

如果你只需要获取一个对象的属性列表以备程序将来使用,ECMAScript 5 引入了 Object.keys()方法,它可以获取可枚举属性的名字的数组

如下使用了 Object.keys()获取了某一对象的可枚举属性。然后用 for 循环遍历属性并输出它们的名字和值。通常操作一个属性名数组时会选用 Object.keys(),而当你不需要数组时则会选用 for-in。

var object = {
  age: 18,
  address: "china",
  sayAge: function() {
    console.log(this.age);
  }
};
var properties = Object.keys(object);
console.log(properties); // [ 'age', 'address', 'sayAge' ]

// 如果你想模仿 for in 行为
var i, len;
for (i = 0, len = properties.length; i < len; i++) {
  console.log("Name:" + properties[i]);
  console.log("Value:" + object[properties[i]]);
}

注意:for-in 循环返回的和 Object.keys() 返回的可枚举属性有一个区别。for-in 循环同时也会遍历原型属性而 Object.keys()只返回自有(实例)属性。第 4 章会讨论原型属性和自有属性的区别。

并不是所有的属性都是可枚举的。实际上,对象的大部分原生方法的[[Enumerable]]特征都被置为 false。你可以用 propertyIsEnumerable()方法检查一个属性是否为可枚举的。每个对象都拥有该方法

var person1 = {
  name: "尼古拉斯"
};

//属性name是可枚举的,因为它是person1上的自定义属性
console.log("name" in person1); // true
console.log(person1.propertyIsEnumerable("name")); // true

// properties数组的属性length则是不可枚举的,因为它是Array.prototype的内建属性。
var properties = Object.keys(person1);
console.log("length" in properties); // true
console.log(properties.propertyIsEnumerable("length")); // false

你会发现很多原生属性默认都是不可枚举的

3.5 属性类型

属性有两种类型:数据属性和访问器属性。数据属性包含一个值;访问器属性不包含值而是定义了一个当属性被读取时调用的函数(称为 getter)和一个当属性被写入时调用的函数(称为 setter)访问器属性仅需要 getter 或 setter 两者中的任意一个,当然也可以两者都有。

[[Put]]方法的默认行为就是创建数据属性,到目前为止用到的前面所有例子使用的都是数据属性

var person = {
  _name: "尼古拉斯", // 表示该属性被认为是私有的,实际上它还是公开的。
  get name() {
    console.log("读取name属性");
    return this._name;
  },
  set name(newValue) {
    console.log("设置新的name属性");
    this._name = newValue;
  }
};

console.log(person.name);
person.name = "新尼古拉斯";
console.log(person.name);

/*
  读取name属性
  尼古拉斯
  设置新的name属性
  读取name属性
  新尼古拉斯
*/

如果你只是需要保存数据,通常没有什么理由使用访问器属性——直接使用属性本身即可。但当你希望赋值操作会触发一些行为或读取的值需要通过计算所需的返回值得到时,访问器属性会非常有用。

3.6 属性特征

在 ECMAScript 5 之前没有办法指定一个属性是否可枚举。实际上根本没有办法访问属性的任何内部特征。为了改变这点,ECMAScript 5 引入了多种方法来和属性特征直接互动,同时也引入新的特征来支持额外的功能。现在可以创建出和 JavaScript 内建属性一样的自定义属性

3.6.1 通用特征

即:数据和访问器属性都拥有的属性特征

一个是[[Enumerable]],决定了你是否可以遍历该属性。另一个是[[Configurable]],决定了该属性是否可配置。你声明的所有属性默认都是可枚举、可配置的

可以使用 Object.defineProperty()方法改变属性特征,参数为

如下代码案例一样

  1. 定义了 name 属性,然后设置它的[[Enumerable]]特征为 false。
  2. 基于这个新值的 propertyIsEnumerable()方法将返回 false。之后 name 被改为不可配置。
  3. 从现在起,由于该属性不能被改变,试图删除 name 将会失败,所以 name 依然存在 instanceObject 中。
  4. 对 name 再次调用Object.defineProperty()instanceObject 对象的属性 name 被有效地锁定
// 假设你想要让一个对象属性变成不可枚举且不可配置
var instanceObject = {
  name: "尼古拉斯",
  age: 18
};
console.log(instanceObject.propertyIsEnumerable("name")); // true

// 1. 将属性name设置为不可枚举
Object.defineProperty(instanceObject, "name", {
  enumerable: false
});

console.log("name" in instanceObject); // true
console.log(instanceObject.propertyIsEnumerable("name")); // 2. false name变成不可枚举

var properties = Object.keys(instanceObject);
console.log(properties); // ['age']

//3.设置name属性不可修改
Object.defineProperty(instanceObject, "name", {
  configurable: false
});

//3.然后尝试删除属性name
delete instanceObject.name;
console.log("name" in instanceObject); // true   name 依然存在于对象中
console.log(instanceObject.name); // 尼古拉斯

// 4. 对象的属性 name 被有效地锁定 ,再次调用Object.defineProperty会报错:
// TypeError: Cannot redefine property: name
Object.defineProperty(instanceObject, "name", {
  configurable: true
});

最后几行代码试图重新定义 name 为可配置的。然而这将抛出错误。你无法将一个不可配置的属性变成可配置。同样,在不可配置的情况下试图将数据属性变为访问器属性或反向变更也会抛出错误。

3.6.2 数据属性特征

数据属性额外拥有两个访问器属性不具备的特征。

第一个是[[Value]],包含属性的值。当你在对象上创建属性时该特征被自动赋值。所有的属性的值都保存在[[Value]]中,哪怕该值是一个函数。

第二个特征是[[Writable]],该特征是一个布尔值,指示该属性是否可以写入。所有的属性默认都是可写的,除非你另外指定。

// 可以使用Object.defineProperty()完整定义一个数据属性,即使该属性还不存在。

// var person11 = {
//   name: "尼古拉斯"
// };

//你可以用下面(更加罗嗦)的代码达到同样的效果。
var person1 = {};

Object.defineProperty(person1, "name", {
  value: "尼古拉斯",
  enumerable: true,
  configurable: true,
  writable: true
});

console.log(person1.name);

上面代码中,当 Object.defineProperty()被调用时,它首先检查属性是否存在。如果不存在,将根据属性描述对象指定的特征创建

注意:当你用 Object.defineProperty()定义新的属性时一定记得为所有的特征指定一个值,否则布尔型的特征会被默认设置为 false。

// 下列代码创建的name属性就是不可枚举、不可配置、不可写的
// 这是因为在调用Object.defineProperty()时没有显式指定这些特征为true
var person2 = {};
Object.defineProperty(person2, "name", {
  value: "Eastboat"
});

console.log("name" in person2); // true
console.log(person2.propertyIsEnumerable("name")); // false  不可枚举

delete person2.name;
console.log("name" in person2); // true  证明name属性删除失败
person2.name = "Greg";
console.log(person2.name); //Eastboat 只能读取属性,其他的操作都被锁定了

3.6.3 访问器属性的特征

访问器属性也有两个额外特征。访问器属性不需要存储值,因此也就没有[[Value]][[Writable]]。取而代之的是[[Get]][[Set]],内含 getter 和 setter 函数。和对象字面形式的 getter 和 setter 一样,仅需要定义其中一个特征就可以创建一个访问器属性。

注意:如果你试图创建一个同时具有数据特征和访问器特征的属性,将会得到一个错误。
使用访问器属性特征比使用对象字面形式定义访问器属性的优势在于,你可以为已有的对象定义这些属性。如果你想要用对象字面形式,你只能在创建对象时定义访问器属性。

和数据属性一样,可以指定访问器属性是否为可配置、可枚举,如下

var person1 = {
  _name: "尼古拉斯",
  get name() {
    console.log("读取name属性");
    return this._name;
  },
  set name(newValue) {
    console.log("设置name属性", newValue);
    this._name = newValue;
  }
};
console.log(person1.name);
/*
读取属性
尼古拉斯
*/

// 这段代码可以被改写成如下形式。
var person1 = {
  _name: "尼古拉斯"
};
Object.defineProperty(person1, "name", {
  get: function() {
    console.log("读取name属性");
    return this._name;
  },
  set: function(newValue) {
    console.log("设置name属性", newValue);
    this._name = newValue;
  },
  enumerable: true, // 可配置
  configurable: true // 可枚举
});
console.log(person1.name);

设置[[Enumerable]]和[[Configurable]]可以改变访问器属性的工作方式。例如,你可以像下面的代码一样来创建一个不可配置、不可枚举、不可写的属性

其中 name 属性是一个只有 getter 的访问器属性。没有 setter,也没有任何特征被显式指定为 true,所以它的值只能被读取,不能被改变。

var person1 = {
  _name: "尼古拉斯"
};
Object.defineProperty(person1, "name", {
  get: function() {
    console.log("Reading name");
    return this._name;
  }
});
console.log("name" in person1); // true
console.log(person1.propertyIsEnumerable("name")); // false
delete person1.name;
console.log("name" in person1); // true
console.log(person1.name); // 尼古拉斯
person1.name = "Greg";
console.log(person1.name); // 尼古拉斯

3.6.4 定义多重属性

Object.defineProperties()可以为一个对象同时定义多个属性。这个方法接受两个参数:需要改变的对象和一个包含所有属性信息的对象。后者可以被看成一个哈希表,键是属性名,值是为该属性定义特征的属性描述对象

如下定义了_name 数据属性和 name 访问器属性。你可以用 Object.defineProperties()定义任意数量的属性。甚至可以同时改变已有的属性并创建新属性。

var person1 = {};

Object.defineProperties(person1, {
  // 数据属性
  _name: {
    value: "Nicholas",
    enumerable: true,
    configurable: true,
    writable: true
  },
  // 访问器属性
  name: {
    get: function() {
      console.log("reading name");
      return this._name;
    },
    set: function(newValue) {
      this._name = newValue;
    },
    enumerable: true,
    configurable: true
  }
});

console.log(person1); // { _name: 'Nicholas', name: [Getter/Setter] }
console.log(person1._name, person1.name); // Nicholas Nicholas

3.6.5 获取属性特征

如果需要获取属性的特征,在 JavaScript 中可以用Object.get OwnPropertyDescriptor()方法。

var person1 = {
  name: "Nicholas"
};

console.log(Object.getOwnPropertyDescriptor(person1, "name"));

/*
 { value: 'Nicholas',
  writable: true,
  enumerable: true,
  configurable: true }
*/

以上代码中,属性 name 作为对象字面形式的一部分被定义。调用Object.getOwnPropertyDescriptor()方法返回的属性描述对象具有 4 个属性:enumerable、configurable、writable 和 value,即使它们从没有被 Object.defineProperty()显式定义

3.7 禁止修改对象

对象和属性一样具有指导其行为的内部特征。其中,[[Extensible]]是一个布尔值,它指明该对象本身是否可以被修改。你创建的所有对象默认都是可扩展的,意味着新的属性可以随时被添加。相信你已经在本章很多地方看到了这一点。设置[[Extensible]]为 false,你就能禁止新属性的添加

有下列 3 种方法可以帮助你锁定对象属性

3.7.1 禁止扩展

第一种方法是用Object.preventExtensions()创建一个不可扩展的对象。该方法接受一个对象作为参数,一旦在一个对象上使用该方法,就永远不能再给它添加新的属性了,可以用 Object.isExtensible()来检查[[Extensible]]的值

如下创建 person1 后,这段代码先检查其[[Extensible]]特征,然后将其变得不可扩展。由于不可扩展,sayName()方法永远无法被加到 person1 上

var person1 = {
  name: "尼古拉斯"
};

console.log(Object.isExtensible(person1)); // true

Object.preventExtensions(person1); // 将person1对象设置为不可扩展低下
console.log(Object.isExtensible(person1)); // false

person1.sayName = function() {
  console.log(this.name);
};
// 由于不可扩展,sayName()方法永远无法被加到person1上
console.log("sayName" in person1); // false

3.7.2 对象封印

第二种方法是对象封印,一个被封印的对象是不可扩展的且其所有属性都不可配置。这意味着不仅不能给对象添加新属性,也不能删除属性或改变其类型(从数据属性变成访问器属性或相反)。如果一个对象被封印,只能读写它的属性

可以用 Object.seal()方法来封印一个对象。该方法被调用时,[[Extensible]]特征被置为 false,其所有属性的[[Configurable]]特征被置为 false。如下面所示,可以用Object.isSealed()判断一个对象是否被封印。

var person1 = {
  name: "尼古拉斯"
};

console.log(Object.isExtensible(person1)); // true 此对象可以被修改
console.log(Object.isSealed(person1)); // false  没有封印

Object.seal(person1);
console.log(Object.isExtensible(person1)); // false  此对象不可被修改
console.log(Object.isSealed(person1)); // true  已经被封印

// 尝试添加新的方法失败
person1.sayName = function() {
  console.log(this.name);
};
console.log("sayName" in person1); // false

// 虽然person1.name被成功改变为一个新值,但是删除它将会失败
person1.name = "Greg";
console.log(person1.name); // Greg

delete person1.name;
console.log("name" in person1); // true

console.log(person1.name); // Greg

console.log(Object.getOwnPropertyDescriptor(person1, "name"));
/**
 { value: 'Greg',
  writable: true,
  enumerable: true,
  configurable: false }
*/

如果你熟悉 Java 或 C++语言,你也应该熟悉被封印对象。当你基于这两种语言的类创建对象时,无法给对象添加新的属性,但可以修改该属性的值。实际上,封印对象就是 JavaScript 在没有类的情况下允许你做同样的控制

注意: 确保对被封印对象使用严格模式,这样当有人误用该对象时,你会得到一个错误。

3.7.3 对象冻结

如果一个对象被冻结,则不能在其上添加或删除属性,不能改变属性类型,也不能写入任何数据属性。

简而言之,被冻结对象是一个数据属性都为只读的被封印对象。被冻结对象无法解冻。可以用Object.freeze()来冻结一个对象。用Object.isFrozen()来判断一个对象是否被冻结

如下代码中:person1 被冻结。被冻结对象也被认为是不可扩展对象和被封印对象,所以 Object.isExtensible()返回 false,而 Object.isSealed()则返回 true。属性 name 无法被改变,所以试图对其赋值为“Greg”的操作失败,后续的检查依旧返回“Nicholas”。

var person1 = {
  name: "Nicholas"
};
console.log(Object.isExtensible(person1)); // true
console.log(Object.isSealed(person1)); // false
console.log(Object.isFrozen(person1)); // false
Object.freeze(person1); // 冻结对象person1
console.log(Object.isExtensible(person1)); // false  变成不可扩展对象
console.log(Object.isSealed(person1)); // true 已经被封印
console.log(Object.isFrozen(person1)); // true  已经被冻结
person1.sayName = function() {
  console.log(this.name);
};
console.log("sayName" in person1); // false
person1.name = "Greg";
console.log(person1.name); // "Nicholas"
delete person1.name;
console.log("name" in person1); // true
console.log(person1.name); // "Nicholas"
var descriptor = Object.getOwnPropertyDescriptor(person1, "name");
console.log(descriptor);
/*
{ value: 'Nicholas',
  writable: false,
  enumerable: true,
  configurable: false }
*/

注意:被冻结对象仅仅只是对象在某个时间点上的快照。其用途有限且极少被使用。和所有不可扩展对象一样,应该对被冻结对象使用严格模式

总结

  1. 将属性视为键值对,对象视为属性的哈希表有助于理解 JavaScript 对象。你可以使用点号或中括号访问对象的属性。可以随时用赋值的方式添加新属性,也可以在任何时候用 delete 操作符删除一个属性。你可以随时用 in 操作符检查对象中某个属性是否存在。如果是自有属性,还可以用 hasOwnProperty(),这个方法存在于所有对象中。所有对象属性默认都是可枚举的,这意味着它们会出现在 for-in 循环中或者被 Object.keys()获取。
  2. 属性有两种类型:数据属性和访问器属性。数据属性保存值,你可以读写它们。当数据属性保存了一个函数的值,该属性被认为是对象的一个方法。不同于数据属性,访问器属性不保存值;它们用 getter 和 setter 来进行指定的操作。可以用对象字面形式创建数据属性和访问器属性。
  3. 所有属性都有一些相关特征。这些特征定义了属性的工作模式。数据属性和访问器属性都具有[[Enumerable]]和[[Configurable]]特征。数据属性还具有[[Writable]]和[[Value]]特征,而访问器属性则具有[[Get]]和[[Set]]特征。[[Enumerable]]和[[Configurable]]默认对所有属性置为 true,[[Writable]]默认对数据属性置为 true。你可以用 Object.defineProperty()或 Object.defineProperties()改变这些特征,用 Object.getOwnPropertyDescriptor()获取它们。
  4. 有 3 种方式可以锁定对象的属性。Object.prevent Extensions()方法创建不可扩展的对象,无法在其上添加新的属性。如果你用 Object.seal()方法创建被封印对象,它不可扩展且其属性不可配置。Object.freeze()方法创建被冻结对象,它同时是一个被封印对象且其数据属性不可写。你要当心这些不可扩展对象并始终对它们使用严格模式,这样任何对其错误的使用都会抛出一个错误。
上一篇下一篇

猜你喜欢

热点阅读