Javascript创建对象方式之原型模式

  • 每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。
  • 实际上,这个对象就是通过调用构造函数创建的对象的原型。
  • 使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。
  • 原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。
  function Person () {}
  Person.prototype.name = 'luke';
  Person.prototype.age = 29;
  Person.prototype.job = 'SE';
  Person.prototype.sayName = function () {
    console.log(this.name);
  };

let person 1 = new Person();
person1.sayName(); // 'luke'

let person2 = new Person();
person2.sayName(); // 'luke'

console.log(person1.sayName === person2.sayName); // true
  • 使用函数表达式也是可以的;
  let Person = function () {};

  // 下面操作相同
  • 所有属性和方法都直接添加到了Person的prototype属性上,构造函数体种啥也没有。但调用构造函数创建的新对象仍然拥有相应的属性和方法。
  • 使用原型模式定义的属性和方法是由所有实例共享的。
  1. 理解原型
    • 只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数。对于前面的例子,Person.prototype.constructor指向Person.
    • 在自定义构造函数时,原型对象默认只会获得constructor属性,其他的所有方法都继承自Object。每次调用构造函数创建一个新实例,这个实例的内部[[prototype]]指针就会被赋值为构造函数的原型对象。
      • 脚本种没有访问这个特性的标准方式,但Chrome和Firefox会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型。
    • 【关键】实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
  • 通过下面的代码来理解原型的行为:
  // 构造函数可以是函数表达式或函数声明
  function Person () {}

  // 声明之后,构造函数就有了一个与之关联的原型对象;
  typeof Person.prototype; // 'object'
  Person.prototype; // {constructor: f Person(), __proto__: Object}

  // 如前所述,构造函数有一个prototype属性,
  // 引用其原型对象,而这个原型对象也有一个
  // constructor属性,引用这个构造函数
  // 换句话说,两者循环引用:

  Person.prototype.constructor === Person; // true

  // 正常的原型都会终止于Object的原型对象
  // Object原型的原型是null
  Person.prototype.__proto__ === Object.prototype; // true
  Person.prototype.__proto__.constructor === Object; // true
  Person.prototype.__proto__.__proto__ === null; // true

  Person.prototype.__proto__; // { constructor: f Object(), toString: ... , hasOwnProperty:..., ...}

  let p1 = new Person();
  let p2 = new Person();

  // 构造函数、原型对象、实例是三个完全不同的对象
  p1 !== Person; // true
  p1 !== Person.prototype; // true
  Person.prototype !== Perosn; // true

  // 实例通过__proto__连接到原型对象,它实际上指向隐藏特性`[[Prototype]]`
  // 构造函数通过prototype属性连接到原型对象
  // 实例与构造函数没有直接联系,与原型对象有直接联系

  p1.__proto__ === Person.prototype; // true
  p1.__proto__.constructor === Person; // true

  // 同一个构造函数创建的两个实例共享同一个原型对象
  person1.__proto__ === person2.__proto__; // true

  // instanceof 检查实例的原型链中是否包含指定构造函数的原型
  p1 instanceof Person // true
  p1 instanceof Object // true
  Person.prototype instanceof Object // true 
  • 对于上例,Person构造函数、Person.prototype与实例之间的关系图:

    • 在这里插入图片描述

    • 上图展示了Person构造函数、Person的原型对象和Person现有两个实例之间的关系。

    • 注意,Person.prototype指向原型对象,而Person.prototype.contructor指回Person构造函数。

    • 原型对象包含constructor属性和其他后来添加的属性。

    • Person的两个实例person1和person2都只有一个内部属性指回Person.prototype,而且两者都与构造函数没有直接联系。

    • 另外要注意,虽然这两个实例都没有属性和方法,但person1.sayName()可以正常调用。这是由于对象属性查找机制的原因。

  • isPrototypeOf()方法确定两个对象之间的这种关系。本质上,isPrototypeOf()会在传入参数的[[Prototype]]指向调用它的对象时返回true。

  Person.prototype.isPrototypeOf(p1); // true
  • 因为p1内部都有链接指向Person.prototype,所以返回true。

  • ES的Object类型有一个方法叫Object.getPrototypeOf(),返回参数的内部特性[[Prototype]]的值。

  Object.getPrototypeOf(p1) == Person.prototype; //true
  Object.getPrototypeOf(p1).name; // 'luke'
  • Object类型还有一个setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一个新值。这样就可以重写一个对象的原型继承关系:
  let biped = {
    numLegs: 2
  };
  let person = {
    name: 'Matt'
  };

  Object.setPrototypeOf(person, biped);

  person.name; // Matt
  person.numLegs; // 2
  person.getPrototypeOf(person) === biped; // true
  • 【警告】Object.setPrototypeOf()可能会严重影响代码的性能。
  • 为了避免使用Object.setPrototype()可能造成的性能下降,可以通过Object.create()来创建一个新对象,同时为其指定原型:
  let biped = {
    numLegs: 2
  };

  let person = Object.create(biped);
  person.name; // 'Matt'
  person.numLegs; // 2
  Object.getPrototypeOf(person) === biped // true
  1. 原型层级
    • 在通过对象访问属性时,会按照如下步骤进行搜索:
      • 1.搜索对象实例本身,若发现给定的名称,返回该名称对应的值。如果没有发现则进行第二部;
      • 2.搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。
  • constructor属性只存在于原型对象,因此通过实例对象也是可以访问的。

  • 可以通过实例读取原型对象上的值,但不能通过实例重写这些值。如果在实例上添加一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会遮住shadow原型对象上的属性。

  • 只要给对象实例添加一个属性,这个属性就会遮蔽(shadow)原型对象上的同名属性,也就是虽然不会修改它,但会屏蔽对它的访问。即使在实例上把这个属性设置为null,也不会恢复它和原型的联系。不过,使用delete操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。

  • hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。该方法继承自Objectde1,会在属性存在于调用它的对象实例上时返回true:

  function Person () {}

  Person.prototype.name = 'luke';
  Person.prototype.age = 26;
  Person.prototype.job = 'SE';
  Person.prototype.sayName = function () {
    console.log(this.name)
  };

  let p1 = new Person();
  let p2 = new Person();
  p1.hasOwnProperty('name'); // false

  p1.name = 'Greg';
  p1.name; // 'Greg'
  p1.hasOwnProperty('name'); // true , name来自实例
  • ECMAScript的Object.getOwnPropertyDescriptor()方法只对实例属性有效。
  • 要取得原型属性的描述符,就必须直接在原型对象上调用Object.getOwnPropertyDescriptor()
  1. 原型和 in 操作符
  • 使用in操作符:
    • 单独使用,in 操作符会在可以通过对象访问指定属性时返回true,无论该属性是在实例上还是在原型上。
    • 在for-in循环中使用。
  function Person () {}

  Person.prototype.name = 'luke';
  Person.prototype.age = 26;
  Person.prototype.job = 'SE';
  Person.prototype.sayName = function () {
    console.log(this.name)
  };

  let p1 = new Person();
  let p2 = new Person();

  p1.hasOwnProperty('name'); // false 来自原型对象
  "name" in p1; // true

  p1.name = '李云龙';
  p1.hasOwnProperty('name'); // true 来自实例
  'name' in p1; // true

  delete p1.name; 
  p1.hasOwnProperty('name'); // false
  'name' in p1; // true
  • name随时可以通过实例或通过原型访问到。因此,调用"name" in persoon1时始终返回true,无论这个属性是否在实例上。如果要确定某个属性是否存在于原型上,则可以像下面这样同时使用hasOwnProperty()和in操作符:
  function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
  }
  • 只要in操作符返回true且hasOwnProperty()返回false,就说明该属性是一个原型属性.

  • 在for-in循环中使用in操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。

  • 遮蔽原型中不可枚举([[Enumerable]]特性被设置为false)属性的实例属性也会在for-in循环中返回,因为默认情况下开发者定义的属性都是可枚举的。

  • 使用Object.keys()方法来获取对象上所有可枚举的实例属性。该方法接收一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串组。

  function Person () {}

  Person.prototype.name = 'luke';
  Person.prototype.age = 26;
  Person.prototype.job = 'SE';
  Person.prototype.sayName = function () {
    console.log(this.name)
  };

  let keys = Object.keys(Person.prototype); // ['name','age,'job','sayName']

  let p1 = new Person();
  p1.name = 'dd';
  p1.age = 27;
  let p1Keys = Object.keys(p1); // ['name', 'age']
  // 而在Person的实例上调用时,Object.keys()返回的数组中只包含"name"和"age"两个属性
  • 如果想列出所有实例属性,无论是否可以枚举,都可以使用Object.getOwnPropertyNames();
  • constructor属性不可枚举。
  • ES6新增Symbol之后,新增了Object.getOwnPropertySymbols()方法,该方法与Object.getOwnPropertyNames()类似。
  let k1 = Symbol('k1'), k2 = Symbol('k2');
  let o = {
    [k1]: 'k1',
    [k2]: 'k2'
  };

  Object.getOwnPropertySymbols(o); // [Symbol(k1), Symbol(k2)]
  1. 属性枚举顺序
  • for-in循环
  • Object.keys()
  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.assign()
  • 在属性枚举顺序方面有很大不同。
  • for-in循环和Object.keys()的枚举顺序是不确定的。取决于js引擎。
  • Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Object.assign()的枚举顺序是确定性的。
    • 先以升序枚举数值键,
    • 然后以插入顺序枚举字符串和符号键。
    • 在对象字面量中定义的键以它们逗号分隔的顺序插入。
  let k1 = Symbol('k1'), k2 = Symbol('k2');
  let o = {
    1: 1,
    first: 'first',
    [k1]: 'k1',
    second: 'second',
    0: 0
  };
  o[k2] = 'sym2';
  o[3] = 3;
  o.third = 'third';
  o[2] = 2

  Object.getOwnPropertyNames(o); 
  // ['0', '1', '2', '3', 'first', 'second', 'third']

  Object.getOwnPropertySymbols(o);
  // [symbol(k1), symbol(k2)]

  object.assign(o);
  /**
      {
        '1': 1,
        '2': 2,
        '3': 3,
        'first': 'first',
        'second': 'second'
        third: 'third',
        [Symbol(k1)]: 'k1',
        [Symbol(k2)]: 'sym2'
      }
  */
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值