JavaScript

JS面向对象精要(四)_构造函数和原型对象

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


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

构造函数和原型对象

由于 JavaScript 缺乏类,它用构造函数和原型对象来给对象带来与类相似的功能。但是,这些相似的功能并不一定表现的跟类完全一致

4.1 构造函数

构造函数就是你用 new 创建对象时调用的函数, 如 js 内建对象的构造函数,Object,Array,Function。 使用构造函数的目的是为了轻松创建许多拥有相同属性和方法的对象

function Person() {}

var person1 = new Person();
var person2 = new Person();

console.log(person1); // Person {}
console.log(person2); // Person {}

console.log(person1 instanceof Person); // true
console.log(person2 instanceof Person); // true

即使 Person 构造函数并没有显式返回任何东西,person1 和 person2 都会被认为是一个新的 Person 类型的实例。new 操作符会自动创建给定类型的对象并返回它们。这也意味着,你可以用 instanceof 操作符获取对象的类型

通过对象字面形式或 Object 构造函数创建出来的泛用对象,其构造函数属性指向 Object;那些通过自定义构造函数创建出来的对象,其构造函数属性指向创建它的构造函数

console.log(person1.constructor === Person); // true
console.log(person2.constructor === Person); // true

使用构造函数的目的是为了轻松创建许多拥有相同属性和方法的对象。为此,你只需要在构造函数内简单地给 this 添加任何你想要的属性即可,当你调用构造函数时,new 会自动创建 this 对象,且其类型就是构造函数的类型。(在本例中为 Person)构造函数本身不需要返回一个值,new 操作符会帮你返回。

function Person(name) {
  this.name = name;
  this.sayName = function() {
    console.log(this.name);
  };
}

var person1 = new Person("尼古拉斯1");
var person2 = new Person("尼古拉斯2");

console.log(person1); // Person { name: '尼古拉斯1', sayName: [Function] }
console.log(person2); // Person { name: '尼古拉斯2', sayName: [Function] }

注意:你也可以在构造函数中显式调用 return。如果返回的值是一个对象,它会代替新创建的对象实例返回。如果返回的值是一个原始类型,它会被忽略,新创建的对象实例会被返回

function Person(name) {
  this.name = name;
  this.sayName = function() {
    console.log(this.name);
  };
  return 123456789; // 返回值原始属性,被忽略
}
var person1 = new Person("尼古拉斯");
console.log(person1); // Person { name: '尼古拉斯', sayName: [Function]

// =====================================

function Person(name) {
  this.name = name;
  this.sayName = function() {
    console.log(this.name);
  };
  return {
    name: "显示返回的对象"
  };
}
var person1 = new Person("尼古拉斯");

// 返回的值是一个对象 ,会代替new出的新的实例
console.log(person1); // { name: '显示返回的对象' }

构造函数允许你用一致的方式初始化一个类型的实例,在使用对象前设置好所有的属性,你也可以在构造函数中用 Object.defineProperty()方法来帮助我们初始化。

function Person(name) {
  // name属性是一个访问者属性,利用name参数来存取实际的值。
  // 之所以能这样做,是因为命名参数就相当于一个本地变量。
  Object.defineProperty(this, "name", {
    get: function() {
      return name;
    },
    set: function(newValue) {
      name = newValue;
    },
    enumerable: true,
    configurable: true
  });
  this.sayName = function() {
    console.log(this.name);
  };
}
var person1 = new Person("尼古拉斯");
person1.sayName(); // 尼古拉斯

console.log(person1); // Person { name: [Getter/Setter], sayName: [Function] }

始终确保用 new 调用构造函数;否则,你就是在冒改变全局对象的风险,而不是创建一个新的对象。

var person2 = Person("Eastboat");
console.log(person2 instanceof Person); // false
console.log(typeof person2); // undefined
console.log(name); // Eastboat

分析如上代码:

注意:在严格模式下,当你不通过 new 调用 Person 构造函数时会出现错误。这是因为严格模式并没有为全局对象设置 this。this 保持为 undefined,而当你试图为 undefined 添加属性时都会出错。

构造函数缺点:构造函数允许你给对象配置同样的属性,但是构造函数并没有消除代码冗余。在之前的例子中,每一个对象都有自己的 sayName()方法。这意味着如果你有 100 个对象实例,你就有 100 个函数做相同的事情,只是使用的数据不同,如果所有的对象实例共享同一个方法会更有效率(原型对象)

4.2 原型对象

可以把原型对象看作是对象的基类。几乎所有的函数(除了一些内建函数)都有一个名为 prototype 的属性,该属性是一个原型对象用来创建新的对象实例。所有创建的对象实例共享该原型对象,且这些对象实例可以访问原型对象的属性。例如,hasOwnProperty()方法被定义在泛用对象 Object 的原型对象中,但却可以被任何对象当作自己的属性访问

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

console.log("name" in person); // true
console.log(person.hasOwnProperty("name")); // true
console.log("hasOwnProperty" in person); // true
console.log(person.hasOwnProperty("hasOwnProperty")); // false

console.log(Object.prototype.hasOwnProperty("hasOwnProperty")); // true

即使 book 中并没有 hasOwnProperty()方法的定义,仍然可以通过 book.hasOwnProperty()访问该方法。这是因为该方法的定义存在于 Object.prototype 中。in 操作符对原型属性和自有属性都返回 true。

你可以用这样一个函数去鉴别一个属性是否是原型属性。

// 如果某个属性in一个对象,但hasOwnProperty()返回false,那么这个属性就是一个原型属性。
function hasPrototypeProperty(object, name) {
  return name in object && !object.hasOwnProperty(name);
}

// 接上一个对象字面量...
// ....
Object.prototype.age = 18;
console.log(hasPrototypeProperty(person, "name")); // false
console.log(hasPrototypeProperty(person, "age")); // true

4.2.1 [[Prototype]]属性

一个对象实例通过内部属性[[Prototype]]跟踪其原型对象。该属性是一个指向该实例使用的原型对象的指针。当你用 new 创建一个新的对象时,构造函数的原型对象会被赋给该对象的[[Prototype]]属性。

  1. 可以调用对象的 Object.getPrototypeOf()方法读取[[Prototype]]属性的值
var person = {};
//任何一个泛用对象,其[[Prototype]]属性始终指向Object.prototype。
var temp = Object.getPrototypeOf(person);
console.log(temp === Object.prototype); // true
  1. 也可以用 isPrototypeOf()方法检查某个对象是否是另一个对象的原型对象,该方法被包含在所有对象中
// object是一个泛用对象,它的原型是Object.prototype
var object = {};
console.log(Object.prototype.isPrototypeOf(object)); // true

当读取一个对象的属性时,JavaScript 引擎首先在该对象的自有属性中查找属性名字。如果找到则返回。如果自有属性中不包含该名字,则 JavaScript 会搜索[[Prototype]]中的对象。如果找到则返回。如果找不到,则返回 undefined。

var object = {};
console.log(object.toString()); // "[object Object]"

// 定义一个自有属性
object.toString = function() {
  return "[object Custom]";
};

console.log(object.toString()); // "[object Custom]" 自有属性会覆盖原型属性

delete object.toString; // 删除自有的属性  (delete操作符仅对自有属性起作用)
console.log(object.toString()); // "[object Object]"

// 继续删除无效-删除仅对自己的属性有效,无法删除原型属性
delete object.toString;
console.log(object.toString()); // "[object Object]"

4.2.2 在构造函数中使用原型对象

原型对象的共享机制使得它们成为一次性为所有对象定义方法的理想手段。因为一个方法对所有的对象实例做相同的事,没理由每个实例都要有一份自己的方法

function Person(name) {
  this.name = name;
}

// 被定义在原型对象上
Person.prototype.sayName = function() {
  console.log(this.name);
};
var person1 = new Person("Nicholas");
var person2 = new Person("Greg");
console.log(person1.name); // "Nicholas"
console.log(person2.name); // "Greg"
person1.sayName(); // outputs "Nicholas"
person2.sayName(); // outputs "Greg"

注意:原型对象上也可以存储引用值,这些引用值会被多个实例共享,可能大家不希望一个实例能够改变另一个实例的值 如下:

function Person(name) {
  this.name = name;
}

Person.prototype.sayName = function() {
  console.log(this.name);
};

Person.prototype.favorites = [];

var person1 = new Person("人物一");
var person2 = new Person("人物二");

person1.favorites.push("人物一追加");
person2.favorites.push("人物二追加");

console.log(person1.favorites); // [ '人物一追加', '人物二追加' ]
console.log(person2.favorites); // [ '人物一追加', '人物二追加' ]

虽然你可以在原型对象上一一添加属性,但是很多开发者会使用一种更简洁的方式:直接用一个对象字面形式替换原型对象,这种方式不需要多次键入 Person.prototype。但是有一个副作用需要注意,如下。

function Person(name) {
  this.name = name;
}

var p = new Person("cc");
console.log(p.constructor); // [Function: Person]

// 使用对象字面形式改写原型对象改变了构造函数的属性,
// 因此它现在指向Object而不是Person
Person.prototype = {
  sayName: function() {
    console.log(this.name);
  },
  toString: function() {
    return "[Person " + this.name + "]";
  }
};
var person1 = new Person("Nicholas");
// constructor属性将被置为泛用对象Object
console.log(person1.constructor); // [Function: Object]
console.log(person1 instanceof Person); // true
console.log(person1.constructor === Person); // false
console.log(person1.constructor === Object); // true

当一个函数被创建时,它的 prototype 属性也被创建,且该原型对象的 constructor 属性指向该函数
当使用对象字面形式改写原型对象 Person.prototype 时,其 constructor 属性将被置为泛用对象 Object。为了避免这一点,需要在改写原型对象时手动重置其 constructor 属性

// 手动调整原型对象的constructor属性
function Person(name) {
  this.name = name;
}

var p = new Person("cc");
console.log(p.constructor); // [Function: Person]

Person.prototype = {
  constructor: Person, // 手动重置指向问题
  sayName: function() {
    console.log(this.name);
  },
  toString: function() {
    return "[Person " + this.name + "]";
  }
};
var person1 = new Person("Nicholas");
console.log(person1.constructor); // [Function: Person]
console.log(person1 instanceof Person); // true
console.log(person1.constructor === Person); // true
console.log(person1.constructor === Object); // false

构造函数、原型对象和对象实例之间的关系最有趣的一个方面也许就是对象实例和构造函数之间没有直接联系。不过对象实例和原型对象以及原型对象和构造函数之间都有直接联系,这样的连接关系意味着,如果打断对象实例和原型对象之间的联系,那么也将打断对象实例和其构造函数的联系

4.2.3 改变原型对象

记住,[[Prototype]]属性只是包含了一个指向原型对象的指针,任何对原型对象的改变都立即反映到所有引用它的对象实例上。这意味着你给原型对象添加的新成员都可以立即被所有已经存在的对象实例使用

function Person(name) {
  this.name = name;
}

Person.prototype = {
  constuctor: Person,
  sayName: function() {
    console.log(this.name);
  },
  toString: function() {
    return "[ Person" + this.name + "]";
  }
};

var person1 = new Person("尼古拉斯111");
var person2 = new Person("尼古拉斯222");

console.log("sayHello" in person1); // false
console.log("sayHello" in person2); // false

// 创建实例之后给原型对象添加新的方法
Person.prototype.sayHello = function() {
  console.log(this.name + "说:你好");
};

console.log("sayHello" in person1); // true
console.log("sayHello" in person2); // true

// 对命名属性的查找是在每次访问属性时发生的,所以可以做到无缝体
person1.sayHello(); // 尼古拉斯111说:你好
person2.sayHello(); // 尼古拉斯222说:你好

可以以随时改变原型对象的能力在封印对象和冻结对象上有一个十分有趣的后果。当你在一个对象上使用 Object.seal()或 Object.freeze()时,完全是在操作对象的自有属性。你无法添加自有属性或改变冻结对象的自有属性,但仍然可以通过在原型对象上添加属性来扩展这些对象实例

function Person(name) {
  this.name = name;
}
Person.prototype = {
  constuctor: Person,
  sayName: function() {
    console.log(this.name);
  },
  toString: function() {
    return "[ Person" + this.name + "]";
  }
};

var person1 = new Person("Nicholas");
var person2 = new Person("Greg");
console.log(person1); // { name: 'Nicholas' }

Object.freeze(person1); // 冻结对象
person1.age = 12; // 添加新得属性age失败
console.log(person1); // { name: 'Nicholas' }

// person1实例被冻结后,继续在原型上添方法
Person.prototype.sayHello = function() {
  console.log(this.name + "说:你好");
};
// person1 和 person2 都获得了这一新方法
person1.sayHello(); // Nicholas说:你好
person2.sayHello(); // Greg说:你好

person1 是冻结对象而 person2 是普通对象。当你在原型对象上添加 sayHi()时,person1 和 person2 都获得了这一新方法,这似乎不符合 person1 的冻结状态。其实,[[Prototype]]属性是对象实例的自有属性,属性本身被冻结,但其指向的值(原型对象)并没有冻结

4.2.4 内建对象的原型对象

所有内建对象都有构造函数,因此也都有原型对象给你去改变。例如,在所有数组上添加一个新的方法只需要简单地修改 Array.prototype 即可。

Array.prototype.sum = function() {
  return this.reduce(function(previous, current) {
    return previous + current;
  }, 0);
};

var numbers = [1, 2, 3, 4, 5, 6];
var result = numbers.sum();
console.log(result); // 21

numbers 数组通过原型对象自动拥有了这个方法。在 sum()内部,this 指向数组的对象实例 numbers,于是该方法也可以自由使用数组的其他方法,比如 reduce()。

你可能还记得字符串、数字和布尔类型都有内建的原始封装类型来帮助我们像使用普通对象一样使用它们。如果改变原始封装类型的原型对象,你就可以给这些原始值添加更多的功能,如下例

这段代码为字符串创建了一个名为 capitalize()的新方法。String 类型是字符串的原始封装类型,修改其原型对象意味着所有的字符串都自动获得这些改动。

String.prototype.capitalize = function() {
  return this.charAt(0).toUpperCase() + this.substring(1);
};

// substring 如果省略第二个参数,那么返回的子串会一直到字符串的结尾

var str = "hello world";
console.log(str.capitalize()); // Hello world

注意:在生产环境中这么做可不是一个好主意。开发者们都期望一个内建对象具有一定的方法并表现出一定的行为。故意改变内建对象会破坏这种期望并导致其他开发者无法确定这些对象会如何工作。

总结

  1. 构造函数就是用 new 操作符调用的普通函数。你可以随时定义你自己的构造函数来创建多个具有同样属性的对象。可以用instanceof操作符或直接访问 constructor 属性来鉴别对象是被哪个构造函数创建的。

  2. 每一个函数都具有 prototype 属性,它定义了该构造函数创建的所有对象共享的属性。通常,共享的方法和原始值属性被定义在原型对象里,而其他属性都定义在构造函数里。constructor 属性实际上被定义在原型对象里供所有对象实例共享。

  3. 原型对象被保存在对象实例内部的[[Prototype]]属性中。这个属性是一个引用而不是一个副本。由于 JavaScript 查找属性的机制,你对原型对象的修改都立刻出现在所有对象实例中。当你试图访问一个对象的某个属性时,JavaScript 首先在自有属性里查找该名字,如果在自有属性中没有找到则查找原型属性。这样的机制意味着原型对象可以随时改变而引用它的对象实例则立即反映出这些改变。

  4. 内建对象也有可以被修改的原型对象。虽然不建议在生产环境中这么做,但它们可以被用来做实验以及验证新功能

上一篇下一篇

猜你喜欢

热点阅读