1.属性类型

ECMA-262 使用一些内部特性来描述属性的特征。这些特性是由为 JavaScript 实现引擎的规范定义 的。因此,开发者不能在 JavaScript 中直接访问这些特性。为了将某个特性标识为内部特性,规范会用 两个中括号把特性的名称括起来,比如[[Enumerable]]。

属性分为两种:数据属性和访问器属性

1.1 数据属性

数据属性包含一个保存数据的位置,值会在这个位置进行读取和写入,有4个特性来描述其行为

  • Configurable :表示属性是否可以通过delete来删除并重新定义,是否可以修改其特性,是否可以改为访问器属性,默认情况下该属性的都为true
  • Enumerable : 表示属性是否可以通过for-in循环返回,默认情况下所有直接定义在对象上的属性的这个特性都是true
  • Writable : 表示属性的值是否可以修改,默认情况下,所有直接定义在对象上的属性的这个特性都是true
  • Value : 包含属性实际的值,这个特性默认为undefined
    1. let person{
    2. name:"chuan"
    3. }
    这里Value的特性会设置为”chuan”,在此之后的所有对name的修改都会保存在这个位置

如果要修改对象的默认特性,需要使用Object.defineProperty()方法。这个方法接受三个参数:

  1. 要添加属性的对象
  2. 属性的名称
  3. 属性包含的特性,其中由{}包含,内可以设置一个或者多个特性

举个栗子

  1. let newObject={};
  2. Object.defineProperty(newObject,'name',{
  3. value:"yuanmou",
  4. writable:false //false为无法修改,true为可修改,默认为true
  5. });
  6. console.log(newObject);
  7. newObject.name="achuan";
  8. console.log(newObject); //"yuanmou"

这个栗子里创建一个只读的name属性,这个属性无法修改,在非严格模式下尝试给此属性赋值会被忽视,而在严格模式下会报错

类似的还有

  1. let person={};
  2. Object.defineProperty(newObject,'name',{
  3. value:"yuanmou",
  4. configurable:false //false为无法删除,true为可修改,默认为true
  5. });
  6. console.log(newObject); //"yuanmou"
  7. delete newObject.name="achuan"; //删除无效
  8. console.log(newObject); //"yuanmou"

设置为false的configurable无法从对象上进行删除,一个属性被定义为不可配置之后,就不能再变回可配置的了。再次调用 Object.defineProperty()并修改任何非 writable 属性会导致错误:

  1. let person={};
  2. Object.defineProperty(newObject,'name',{
  3. value:"yuanmou",
  4. configurable:false
  5. });
  6. //抛出错误
  7. Object.defineProperty(newObject,'name',{
  8. value:"yuanmou",
  9. configurable:true
  10. });
  • 因此,虽然可以对同一个属性多次调用 Object.defineProperty(),但在把 configurable 设 置为 false 之后就会受限制了
  • 。 在调用 Object.defineProperty()时,configurable、enumerable 和 writable 的值如果不 指定,则都默认为 false。
  • 多数情况下,可能都不需要 Object.defineProperty()提供的这些强大的设置,但要理解 JavaScript 对象,就要理解这些概念。

1.2 定义多个属性

在一个对象上同时定义多个属性的可能性是非常大的。为此,ECMAScript 提供了 Object.defineProperties()方法。这个方法可以通过多个描述符一次性定义多个属性。它接收两个参数:要为之添 加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应。

  1. let book={};
  2. Object.defineProperties(book,{
  3. name:{
  4. value:"JavaScript"
  5. },
  6. edition:{
  7. value:1
  8. }
  9. })

2. 增强对象语法

ECMAScript 6 为定义和操作对象新增了很多极其有用的语法糖特性。这些特性都没有改变现有引擎 的行为,但极大地提升了处理对象的方便程度。

2.1 属性值简写

在给对象添加变量的时候,开发者经常会发现属性名和变量名是一样的。例如:

  1. let name='yuanmou';
  2. let person={
  3. name:name
  4. };
  5. console.log(person) // { name: 'yuanmou' }

为此,简写属性名语法出现了。简写属性名只要使用变量名(不用再写冒号)就会自动被解释为同 名的属性键。如果没有找到同名变量,则会抛出 ReferenceError。
以下代码和之前的代码是等价的:

  1. let name='yuanmou';
  2. let person={
  3. name
  4. };
  5. console.log(person) // { name: 'yuanmou' }

代码压缩程序会在不同作用域间保留属性名,以防止找不到引用。以下面的代码为例:

  1. function Person(name) {
  2. return {
  3. name
  4. };
  5. }
  6. let person = Person('yuanmou');
  7. console.log(person.name); // Matt

3. 原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例 共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处 是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以 直接赋值给它们的原型,

  1. function Person() {}
  2. //原型对象添加属性和方法
  3. Person.prototype.name = "Nicholas";
  4. Person.prototype.age = 29;
  5. Person.prototype.job = "Software Engineer";
  6. Person.prototype.sayName = function() {
  7. console.log(this.name);
  8. };
  9. //实例化对象
  10. let person1 = new Person();
  11. person1.sayName(); // "Nicholas"
  12. let person2 = new Person();
  13. person2.sayName(); // "Nicholas"
  14. console.log(person1.sayName == person2.sayName); // true 都使用原型对象里的属性

使用函数表达式也可以:

  1. let Person = function() {}; //函数表达式创建的构造函数也有原型对象
  2. Person.prototype.name = "Nicholas";
  3. Person.prototype.age = 29;
  4. Person.prototype.job = "Software Engineer";
  5. Person.prototype.sayName = function() {
  6. console.log(this.name);
  7. };
  8. let person1 = new Person();
  9. person1.sayName(); // "Nicholas"
  10. let person2 = new Person();
  11. person2.sayName(); // "Nicholas"
  12. console.log(person1.sayName == person2.sayName); // true
  • 这里,所有属性和 sayName()方法都直接添加到了 Person 的 prototype 属性上,构造函数体中 什么也没有。
  • 但这样定义之后,调用构造函数创建的新对象仍然拥有相应的属性和方法。
  • 与构造函数模 式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。
  • 因此 person1 和 person2 访问的 都是相同的属性和相同的 sayName()函数。
  • 要理解这个过程,就必须理解 ECMAScript 中原型的本质。

3.1 理解原型对象

无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向 原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。对前面的例子而言,Person.prototype.constructor 指向 Person。然后,因构造函数而异,可能会给原型对象添加其他属性和方法。

  • 在自定义构造函数时,原型对象默认只会获得 constructor 属性,其他的所有方法都继承自 Object。
  • 每次调用构造函数创建一个新实例,这个实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象。
  • 脚本中没有访问这个[[Prototype]]特性的标准方式,但 Firefox、Safari 和 Chrome 会在每个对象上暴露proto属性,通过这个属性可以访问对象的原型。
  • 在其他实现中,这个特性 完全被隐藏了。关键在于理解这一点:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
  1. /**
  2. * 构造函数可以是函数表达式
  3. * 也可以是函数声明,因此以下两种形式都可以:
  4. * function Person() {}
  5. * let Person = function() {}
  6. */
  7. function Person() {}
  8. /**
  9. * 声明之后,构造函数就有了一个
  10. * 与之关联的原型对象:
  11. */
  12. console.log(typeof Person.prototype);
  13. console.log(Person.prototype);
  14. // {
  15. // constructor: f Person(),
  16. // __proto__: Object
  17. // }
  18. /**
  19. * 如前所述,构造函数有一个 prototype 属性
  20. * 引用其原型对象,而这个原型对象也有一个
  21. * constructor 属性,引用这个构造函数
  22. * 换句话说,两者循环引用:
  23. */
  24. console.log(Person.prototype.constructor === Person); // true
  25. /**
  26. * 正常的原型链都会终止于 Object 的原型对象
  27. * Object 原型的原型是 null
  28. */
  29. console.log(Person.prototype.__proto__ === Object.prototype); // true
  30. console.log(Person.prototype.__proto__.constructor === Object); // true
  31. console.log(Person.prototype.__proto__.__proto__ === null); // true
  32. console.log(Person.prototype.__proto__);
  33. // {
  34. // constructor: f Object(),
  35. // toString: ...
  36. // hasOwnProperty: ...
  37. // isPrototypeOf: ...
  38. // ...
  39. // }
  40. let person1 = new Person(),
  41. person2 = new Person();
  42. /**
  43. * 构造函数、原型对象和实例
  44. * 是 3 个完全不同的对象:
  45. */
  46. console.log(person1 !== Person); // true
  47. console.log(person1 !== Person.prototype); // true
  48. console.log(Person.prototype !== Person); // true
  49. /**
  50. * 实例通过__proto__链接到原型对象,
  51. * 它实际上指向隐藏特性[[Prototype]]
  52. *
  53. * 构造函数通过 prototype 属性链接到原型对象
  54. *
  55. * 实例与构造函数没有直接联系,与原型对象有直接联系
  56. */
  57. console.log(person1.__proto__ === Person.prototype); // true
  58. conosle.log(person1.__proto__.constructor === Person); // true
  59. /**
  60. * 同一个构造函数创建的两个实例
  61. * 共享同一个原型对象:
  62. */
  63. console.log(person1.__proto__ === person2.__proto__); // true
  64. /**
  65. * instanceof 检查实例的原型链中
  66. * 是否包含指定构造函数的原型:
  67. */
  68. console.log(person1 instanceof Person); // true
  69. console.log(person1 instanceof Object); // true
  70. console.log(Person.prototype instanceof Object); // true

yuanxing.png

对象原型

对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。proto对象原型和原型对象 prototype 是等价的 proto对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype

img2.png

constructor构造函数

对象原型( proto)和构造函数(prototype)原型对象里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。 一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。

如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数如:

  1. function Person(uname, age) {
  2. this.uname = uname;
  3. this.age = age;
  4. }
  5. // 很多情况下,我们需要手动的利用constructor 这个属性指回 原来的构造函数
  6. Person.prototype = {
  7. // 如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数
  8. constructor: Person, // 手动设置指回原来的构造函数
  9. sayhi: function() {
  10. console.log('who is ');
  11. },
  12. move: function() {
  13. console.log('where');
  14. }
  15. }
  16. var ym = new Person('袁某', 36);
  17. console.log(ym)

3.1 原型的动态性

因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对 象所做的修改也会在实例上反映出来。

  1. function Person(){}
  2. let friend = new Person();
  3. Person.prototype.sayHi = function() {
  4. console.log("hi");
  5. };
  6. friend.sayHi(); // "hi",没问题!
  • 以上代码先创建一个 Person 实例并保存在 friend 中。
  • 然后一条语句在 Person.prototype 上添加了一个名为 sayHi()的方法。
  • 虽然 friend 实例是在添加方法之前创建的,但它仍然可以访问这个方法。
  • 之所以会这样,主要原因是实例与原型之间松散的联系。在调用 friend.sayHi()时,首先会从 这个实例中搜索名为 sayHi 的属性。
  • 在没有找到的情况下,运行时会继续搜索原型对象。
  • 因为实例和 原型之间的链接就是简单的指针,而不是保存的副本,所以会在原型上找到 sayHi 属性并返回这个属 性保存的函数

  • 虽然随时能给原型添加属性和方法,并能够立即反映在所有对象实例上,但这跟重写整个原型是两 回事。
  • 实例的[[Prototype]]指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同 的对象也不会变。
  • 重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。
  • 记住,实例只有指向原型的指针,没有指向构造函数的指针。
  • 来看下面的例子:
    1. function Person() {}
    2. let friend = new Person();
    3. Person.prototype = {
    4. constructor: Person,
    5. name: "Nicholas",
    6. age: 29,
    7. job: "Software Engineer",
    8. sayName() {
    9. console.log(this.name);
    10. }
    11. };
    12. friend.sayName(); // 错误
    在这个例子中,Person 的新实例是在重写原型对象之前创建的。在调用 friend.sayName()的时 候,会导致错误。这是因为 firend 指向的原型还是最初的原型,而这个原型上并没有 sayName 属性。

4. 原型的问题

原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默 认都取得相同的属性值。虽然这会带来不便,但还不是原型的最大问题。原型的最主要问题源自它的共享特性。
我们知道,原型上的所有属性是在实例间共享的,这对函数来说比较合适。另外包含原始值的属性也还好,如前面例子中所示,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。真正的问题来自包含引用值的属性。

  1. function Person() {}
  2. Person.prototype = {
  3. constructor: Person,
  4. name: "Nicholas",
  5. age: 29,
  6. job: "Software Engineer",
  7. friends: ["Shelby", "Court"],
  8. sayName() {
  9. console.log(this.name);
  10. }
  11. };
  12. let person1 = new Person();
  13. let person2 = new Person();
  14. person1.friends.push("Van");
  15. console.log(person1.friends); // "Shelby,Court,Van"
  16. console.log(person2.friends); // "Shelby,Court,Van"
  17. console.log(person1.friends === person2.friends); // true
  • 这里,Person.prototype 有一个名为 friends 的属性,它包含一个字符串数组。
  • 然后这里创建 了两个 Person 的实例。
  • person1.friends 通过 push 方法向数组中添加了一个字符串。
  • 由于这个 friends 属性存在于 Person.prototype 而非 person1 上,新加的这个字符串也会在(指向同一个 数组的)person2.friends 上反映出来。
  • 如果这是有意在多个实例间共享数组,那没什么问题。但一 般来说,不同的实例应该有属于自己的属性副本。
  • 这就是实际开发中通常不单独使用原型模式的原因。