Javascript面向对象(六)——方法原型

Javascript面向对象(六)——方法原型

本节我们主要说明额外的方法使用原型。

除了我们已经知道的方式,也有其他的方式get/set原型。

  • Object.create(proto[, descriptors])– 使用给定的原型和可选的属性描述符创建一个空对象。
  • Object.getPrototypeOf(obj)– 返回对象原型[[Prototype]].
  • Object.setPrototypeOf(obj, proto) – 设置对象的原型[[Prototype]] 引用 proto.

举例:

let animal = {
  eats: true
};

// create a new object with animal as a prototype
let rabbit = Object.create(animal);

alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // get the prototype of rabbit

Object.setPrototypeOf(rabbit, {}); // change the prototype of rabbit to {}

方法Object.create第二个参数是可选的:属性描述符。我们可以提供额外的属性给新对象,如下代码:

let animal = {
  eats: true
};

let rabbit = Object.create(animal, {
  jumps: {
    value: true
  }
});

alert(rabbit.jumps); // true

描述符与我们之前章节描述格式一样,我们能用Object.create方法执行一个完整对象克隆,像这样:

// fully identical shallow clone of obj
let clone = Object.create(obj, Object.getOwnPropertyDescriptors(obj));

这个调用完全克隆一个对象拷贝,包括所有属性:枚举和非枚举,数据属性及setter/getter——所有内容,并带有正确的原型。但当然不是深复制。

简史

如果我们数以下管理原型的方式,太多了,有太多的方式做相同的事情!
为什么如此?那是历史原因。

  • 构造函数“prototype”属性在很早就开始使用。
  • 在2012年之后:Object.create()方法在标准中出现,允许创建对象是指定原型,但不允许get/set,所以浏览器实现了非标准的__proto__访问器,允许在任何时候get/set原型。
  • 在2015年之后:Object.setPrototypeOfObject.getPrototypeOf 被增加至标准中。__proto__是事实实现无处不在,所以它是附录B的标准,对非浏览器环境是可选的。

现在我们有了所有的方法处理原型。
技术上,我们能get/set原型在任何时候,但通常我们仅在对象创建时设置一次,然后不再修改:rabbit继承自animal,无需改变。
Javascript引擎高度优化了这里,使用Object.setPrototypeOf速度飞快,同样obj.__proto__ =实现同样功能,速度很慢,但可以。

非常普通的对象

我们知道,对象可以作为关联数组存储键值对。
但是,我们尝试存储“用户提供键”(举例,用户输入字典),我们能发现一个有趣的错误,即除了”proto“键,其他所有键都可以。

示例说明:
let obj = {};

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // [object Object], not "some value"!

这里,如果用户输入__proto__,那么赋值被忽略!

我们不应该对此惊讶,__proto__属性是特殊的:他必须是一个对象或null,字符串不能称为原型。
但我们不打算实现这样行为,对吧?我们想去存储键值对,并且键名称为__proto__,也不能被存储。
所以这是一个bug,这里的后果并不可怕。但在其他情况原型可能确实被改变,所以执行出错可能是完全意料之外的。

最糟糕的是,通常开发者根本不考虑这些,让这些错误很难注意到,导致存在漏洞,特别在服务器段Javascript。

这中情况仅发生在__proto__属性,其他属性被正常复杂。如何规避这个问题?

首先,我们能切换使用Map,则一切都好了。

但Object也能实现,因为语言的创建者在很早以前就考虑了这个问题。
__proto__不是对象的一个属性,但能通过Object.prototype属性访问器访问。

Javascript面向对象(六)——方法原型_第1张图片

所以,如果读取或赋值obj.__proto__,原型中相应的setter/getter方法被调用,并设置或获得[[Prototype]]属性。
如前所说,obj.__proto__是访问[[prototype]]的一种方式,不是[[prototype]]自身。
现在如果我们想使用对象作为关联数组,我们可以用点小技巧实现:

let obj = Object.create(null);

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // "some value"

Object.create(null) 创建一个空的对象,没有指定原型(原型为null)。

Javascript面向对象(六)——方法原型_第2张图片

所以,没有从obj.__proto__继承getter/setter方法。现在处理属性作为普通数据,所以上述示例正常运行。
我们能称这样的对象为“非常普通的对象”或“纯字典对象”,因为它们比一般普通对象{...}更简单。

缺点是这样对象缺省任何内置方法,如toString

let obj = Object.create(null);

alert(obj); // Error (no toString)

但对关联数组来说,这通常是好事。

请注意,大多数对象相关的方法是Object.something(...),象Object.keyes(obj)——它们不在原型中,因此它们在普通对象上仍然可以使用:

let chineseDictionary = Object.create(null);
chineseDictionary.hello = "ni hao";
chineseDictionary.bye = "zai jian";

alert(Object.keys(chineseDictionary)); // hello,bye

获取所有属性

有许多方式从对象上获得键/值。我们已经知道这些是:

  • Object.keys(obj) / Object.values(obj)/ Object.entries(obj) – 返回一个数组,包括枚举属性字符串名称/值/键-值对。这些方法仅列出可枚举属性,并且使用字符串做为键的属性。

获取符号属性:

  • Object.getOwnPropertySymbols(obj) – returns an array of all own symbolic property names.

获取非枚举属性:

  • Object.getOwnPropertyNames(obj) – returns an array of all own string property names.

获取所有属性s:

  • Reflect.ownKeys(obj) – returns an array of all own property names.

这些方法返回的属性有一些差异,但所有这些方法作用在对象上,原型中的属性并没有被列出。
循环for..in 不一样,也会列出继承属性,示例:

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// only own keys
alert(Object.keys(rabbit)); // jumps

// inherited keys too
for(let prop in rabbit) alert(prop); // jumps, then eats

如果我们想区分继承属性,有个内置的方法obj.hasOwnProperty(key),如果obj有自己属性(不是继承的)则返回true。
所以我们能过滤出继承属性:

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

for(let prop in rabbit) {
  let isOwn = rabbit.hasOwnProperty(prop);
  alert(`${prop}: ${isOwn}`); // jumps:true, then eats:false
}

这里我们看下面继承链:rabbit,animal,然后Object.prototype(因为animal是普通对象{…},所以这时缺省的),最后是null:

Javascript面向对象(六)——方法原型_第3张图片

注意,有个有趣的事情,rabbit.hasOwnProperty方法来自哪里?观察原型链,能看出Object.prototype.hasOwnProperty,也就是说,是继承来的。
但为什么hasOwnProperty没有出现在for..in的结果中,如果其列出所有继承属性?

答案是简单的:它是非枚举的,就想所有Object.prototype中的其他属性。所以没有被列出来。

总结

这里简要列出本文讨论的方法,作为重述内容。

  • Object.create(proto[, descriptors])
    创建一个空对象,使用参数proto作为其原型(也可以为null),另外还有一个可选的描述符属性参数。

  • Object.getPrototypeOf(obj)
    返回对象的[[Prototype]](与proto获取一样)。

  • Object.setPrototypeOf(obj, proto)
    设置proto给对象的[[Prototype]](与proto 设置一样)

  • Object.keys(obj) / Object.values(obj) / Object.entries(obj)
    返回一个数组,可枚举属性字符名称/值/键值对。

  • Object.getOwnPropertySymbols(obj)
    返回所有符合属性名称的数组。

  • Object.getOwnPropertyNames(obj)
    返回所有拥有的字符串属性名称数组。

  • Reflect.ownKeys(obj)
    返回所有拥有的字符串属性名称数组。

  • obj.hasOwnProperty(key)
    返回true,如果对象有该属性(非继承),反之为false。

我们澄清:
proto是[[Prototype]]的一个getter/setter,象其他方法一样,驻留在Object.prototype中。

我们可以创建一个对象通过Object.create(null),这样对象我们通常称为“纯字典”,他们使用”proto“作为键没有问题。

所有返回对象属性的方法(想Object.keys和其他),返回自己的属性。如果我们想获得继承的属性,那么需使用for...in

你可能感兴趣的:(深入理解Javascript,javascript,prototype,方法原型,字典对象,枚举属性)