JavaScript

JS面向对象精要(五)_继承

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


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

5.1 原型对象链和 Objec.prototype

JavaScript 内建的继承方法被称为原型对象链,又可称为原型对象继承

原型对象的属性可经由对象实例访问,这就是继承的一种形式。对象实例继承了原型对象的属性。因为原型对象也是一个对象,它也有自己的原型对象并继承其属性。这就是原型对象链:对象继承其原型对象,而原型对象继承它的原型对象,依此类推

1.所有对象都继承自 Object.prototype。
2.任何以对象字面形式定义的对象,其[[Prototype]]的值都被设为 Object.prototype,这意味着它继承 Object.prototype 的属性

// book的对象原型是Object.prototype。
var book = {
  title: "The Principles of Object-Oriented JavaScript"
};
var prototype = Object.getPrototypeOf(book);
console.log(prototype === Object.prototype); // true

5.1.1 继承自 Object.prototype 的方法

前面案例用到的多个方法其实都是定义在 Object.prototype 上的。因此可以被其他对象继承。这 5 种方法经由继承出现在所有对象中

hasOwnproperty(); // 检查是否存在一个给定名字的自有属性
propertyIsEnumerable(); // 检查一个自有属性是否可枚举
isPrototypeOf(); // 检查一个对象是否是另一个对象的原型对象
valueOf(); // 返回一个对象的值表达
toString(); // 返回一个对象的字符串表达

1. valueOf()

每当一个操作符被用于一个对象时就会调用 valueOf()方法。valueOf()默认返回对象实例本身。
原始封装类型重写了 valueOf()以使得它对 String 返回一个字符串,对 Boolean 返回一个布尔,对 Number 返回一个数字。类似的,Date 对象的 valueOf()方法返回一个 epoch 时间,单位是毫秒(正如 Date.prototype.getTime()所为)。这允许你写出下例代码来对 Date 做比较。

当使用大于操作符(>)时,在实际比较前,在两个对象上都调用了 valueOf()方法

var now = new Date();
var earlier = new Date(2010, 1, 1);
console.log(now > earlier); // true

如果你的对象也要这样使用操作符,你可以定义自己的 valueOf()方法。定义的时候你并没有改变操作符的行为,仅仅定义了操作符默认行为所使用的值。

2. toString()

一旦 valueOf()返回的是一个引用而不是原始值的时候,就会回退调用 toString()方法。另外,当 JavaScript 期望一个字符串时,也会对原始值隐式调用 toString()。

例如,当加号操作符的一个边是一个字符串时,另一边会被自动转换成字符串。如果另一边是一个原始值,会自动被转换成一个字符串表达(例如,true 转换成“true”),如果另一边是一个引用值,则会调用 valueOf()。如果 valueOf()返回一个引用值,则调用 toString()

var book = {
  title: "The Principles of Object-Oriented JavaScript"
};
/*
    因为book是一个对象,此时调用它的toString()方法。
    该方法继承自Object.prototype,大部分JavaScript引擎返回默认值“[object Object]
*/
var message = "Book = " + book;
console.log(message); // "Book = [object Object]"

定义自己的 toString()方法有时可以为此类字符串转换提供包含更多信息的值

/*
    book自定义的toString()方法与继承来的版本相比,返回更有用的值
*/
var book = {
  title: "The Principles of Object-Oriented JavaScript",
  toString: function() {
    return "[Book " + this.title + "]";
  }
};
var message = "Book = " + book;
// "Book = [Book The Principles of Object-Oriented JavaScript]"

5.1.2 修改 Object.prototype

所有的对象都默认继承自 Object.prototype,所以改变 Object.prototype 会影响所有的对象,这是非常危险的。

添加 Object.prototype.add()会导致所有的对象都有了一个 add()方法,不管这样是不是合理。不仅仅给开发者,同时也给 JavaScript 委员会带来了问题:它不得不把新方法添加到各种不同的地方,因为给 Object.prototype 添加方法可能会带来不可预知的结果。

Object.prototype.add= function(value) {
  return this + value;
};
var book = {
  title:"The Principles of Object-Oriented JavaScript"
};
console.log(book.add(5));         // "[object Object]5"
console.log("title".add("end"));     // "titleend"
// in a web browser
console.log(document.add(true));     // "[object HTMLDocument]true"
console.log(window.add(5));        // "[object Window]true"

这个问题的另一方面在于给 Object.prototype 添加可枚举属性。在之前的例子里,Object.prototype.add()是一个可枚举属性,这意味着它会出现在 for-in 循环中

// 一个空对象依然会输出“add”作为其属性,就是因为它存在于其原型对象里且为可枚举属性
var empty = {};
for (var property in empty) {
  console.log(property);
}

考虑到 JavaScript 中使用 for-in 的频繁程度,为 Object.prototype 添加可枚举属性会影响大量代码。因为这个原因,Douglas Crockford 推荐在 for-in 循环中始终使用 hasOwnProperty(),如下。

var empty = {};
for (var property in empty) {
  if (empty.hasOwnProperty(property)) {
    console.log(property);
  }
}

这个方法虽然可以有效过滤那些不想要的原型对象的属性,但也同时限制了 for-in 循环,使其只能用于自有属性,这也许不是你想要的。对你来说,最灵活的做法还是不要修改 Object.prototype。

5.2 对象继承

对象继承是最简单的继承类型。你唯一需要做的就是指定哪个对象是新对象的[[Prototype]]

对象字面形式会隐式指定Object.prototype为其[[Prototype]],你也可以用Object.create()方法显式指定。

Object.create()方法接受两个参数。第一个参数是需要被设置为新对象的[[Prototype]]的对象(原型对象)。第二个可选参数是一个属性描述对象

var book = {
  title: "The Principles of Object-Oriented JavaScript"
};
// 字面量创建对象 等价如下

var book = Object.create(Object.prototype, {
  title: {
    configurable: true,
    enumerable: true,
    value: "The Principles of Object-Oriented JavaScript",
    writable: true
  }
});
  1. 第一种声明使用对象字面形式来定义一个具有单一属性 title 的对象。该对象自动继承自 Object.prototype,且其属性被默认设置为可配置、可枚举和可写
  2. 第二种声明使用 Object.create()显式做了同样的操作。两个 book 对象的行为完全一致。但你可能永远不会这样写出直接继承自 Object.prototype 的代码,毕竟那是默认行为。下面代码中演示了继承自其他对象
var person1 = {
  name: "Nicholas",
  sayName: function() {
    console.log(this.name);
  }
};

// 对象person2继承自person1,也就继承了name和sayName()

// name自有属性隐藏并代替了原型对象的同名属性
var person2 = Object.create(person1, {
  name: {
    configurable: true,
    enumerable: true,
    value: "Greg",
    writable: true
  }
});

person1.sayName(); // Nicholas
person2.sayName(); //  Greg

console.log(person1.hasOwnProperty("sayName")); // true
// sayName()依然只存在于person1并被person2继承。
console.log(person2.hasOwnProperty("sayName")); //  false
console.log(person1.isPrototypeOf(person2)); // true

当访问一个对象的属性时,JavaScript 引擎会执行一个搜索过程。如果在对象实例上发现该属性(就是说是个自有属性),该属性值就会被使用。如果对象实例上没有发现该属性,则搜索[[Prototype]]。如果仍然没有发现,则继续搜索该原型对象的[[Prototype]],直到继承链末端。末端通常是一个 Object.prototype,其[[Prototype]]被置为 null。

// 通过Object.create()创建[[Prototype]]为null的对象
// 一个没有原型对象的对象
var nakedObject = Object.create(null);
console.log("toString" in nakedObject); // false
console.log("valueOf" in nakedObject); // false

5.3 构造函数继承

JavaScript 中的对象继承也是构造函数继承的基础。还记得第 4 章提到,几乎所有的函数都有一个原型属性 prototype,这个属性是一个指针,指向一个对象,这个对象包含有特定类型的所有实例共享的属性和方法,它可以被修改或替换。该 prototype 属性被自动设置为一个新的继承自 Object.prototype 的泛用对象

这段代码帮你把构造函数的 prototype 属性设置为一个继承自 Object.prototype 的对象。这意味着 Yourconstructor 创建出来的任何对象都继承自 Object.prototype。

// 自定义一个构造函数
function Foo() {
  // do somethings
}
// JavaScript引擎在幕后为您实现这一点
Foo.prototype = Objectcaret(Object.prototype, {
  constructor: {
    configurable: true,
    enumerable: true,
    value: Foo,
    writable: true
  }
});

由于 prototype 属性可写,你可以通过改写它来改变原型对象链。

function Rectangle(length, width) {
  this.length = length;
  this.width = width;
}

Rectangle.prototype.getArea = function() {
  return this.length * this.width;
};

Rectangle.prototype.toString = function() {
  return "[Rectangle " + this.length + "x" + this.width + "]";
};

// 从矩形中继承

function Square(size) {
  this.length = size;
  this.width = size;
}

Square.prototype = new Rectangle();
Square.prototype.constructor = Square;

Square.prototype.toString = function() {
  return "[Square " + this.length + "x" + this.width + "]";
};

var rect = new Rectangle(5, 10);
var square = new Square(6);
console.log(rect.getArea()); // 50
console.log(square.getArea()); // 36
console.log(rect.toString()); // "[Rectangle 5x10]"
console.log(square.toString()); // "[Square 6x6]"
console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Object); // true

// 因为instanceof使用原型对象链检查对象类型。
// 所以instanceof操作符认为变量square同时是Square、Rectangle和Object的对象实例,
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square instanceof Object); // true
image.png

Square.prototype 并不真的需要被改写为一个 Rectangle 对象,毕竟 Rectangle 构造函数并没有真的为 Square 做什么必要的事情。事实上,唯一相关的部分是 Square.prototype 需要指向 Rectangle.prototype,使得继承得以实现。
这意味着你可以用 Object.create()简化例子

function Rectangle(length, width) {
  this.length = length;
  this.width = width;
}

Rectangle.prototype.getArea = function() {
  return this.length * this.width;
};

Rectangle.prototype.toString = function() {
  return "[Rectangle " + this.length + "x" + this.width + "]";
};

// 从矩形中继承

function Square(size) {
  this.length = size;
  this.width = size;
}

Square.prototype = Object.create(Rectangle.prototype, {
  constructor: {
    configurable: true,
    enumerable: true,
    value: Square,
    writable: true
  }
});
// 在对原型对象添加属性前要确保你已经改写了原型对象,否则在改写时会丢失之前添加的方
Square.prototype.toString = function() {
  return "[Square " + this.length + "x" + this.width + "]";
};
var rect = new Rectangle(5, 10);
var square = new Square(6);
console.log(rect.getArea()); // 50
console.log(square.getArea()); // 36
console.log(rect.toString()); // "[Rectangle 5x10]"
console.log(square.toString()); // "[Square 6x6]"
console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Object); // true
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square instanceof Object); // true

在这个版本的代码中,Square.prototype 被改写为一个新的继承自 Rectangle.prototype 的对象,而 Rectangle 构造函数没有被调用。这意味着,你不再需要担心不加参数调用构造函数会导致的错误。除此之外,这段代码和前面的代码行为完全一致。原型对象链完好无缺,所有的 Square 对象实例都继承自 Rectangle.prototype 且其 constructor 属性也都在同样的地方被重置。

注意:在对原型对象添加属性前要确保你已经改写了原型对象,否则在改写时会丢失之前添加的方

5.4 构造函数窃取

由于 JavaScript 中的继承是通过原型对象链来实现的,因此不需要调用对象的父类的构造函数。如果你确实需要在子类构造函数中调用父类构造函数,那你就需要利用 JavaScript 函数工作的特性(call 和 apply)

在第 2 章中学过的 call()和 apply()方法允许你在调用函数时提供一个不同的 this 值。那正好是构造函数窃取的关键。只需要在子类的构造函数中用 call()或者 apply()调用父类的构造函数,并将新创建的对象传进去即可

function Rectangle(length, width) {
  this.length = length;
  this.width = width;
}

Rectangle.prototype.getArea = function() {
  return this.length * this.width;
};

Rectangle.prototype.toString = function() {
  return "[Rectangle " + this.length + "x" + this.width + "]";
};
// 继承矩形
function Square(size) {
  Rectangle.call(this, size, size);
  // optional:add new properties or override existing ones here
}

Square.prototype = Object.create(Rectangle.prototype, {
  constructor: {
    configurable: true,
    enumerable: true,
    value: Square,
    writabel: true
  }
});
Square.prototype.toString = function() {
  return "[Square " + this.length + "x" + this.width + "]";
};
var square = new Square(6);
console.log(square.length); // 6
console.log(square.width); // 6
console.log(square.getArea()); // 36

Square 构造函数调用了 Rectangle 构造函数,并传入了 this 和 size 两次(一次作为 length,另一次作为 width)。这么做会在新对象上创建 length 和 width 属性并让它们等于 size,这是一种避免在构造函数里重新定义你希望继承的属性的手段。你可以在调用完父类的构造函数后继续添加新属性或覆盖已有的属性。

这个分两步走的过程在你需要完成自定义类型之间的继承时比较有用。你经常需要修改一个构造函数的原型对象,你也经常需要在子类的构造函数中调用父类的构造函数。一般来说,需要修改 prototype 来继承方法并用构造函数窃取来设置属性。由于这种做法模仿了那些基于类的语言的类继承,通常被称为伪类继承。

5.5 访问父类方法

5.5  访问父类方法
在前面的例子中,Square 类型有自己的 toString()方法隐藏了其原型对象的 toString()方法。子类提供新功能覆盖父类的方法十分常见,但如果你还想访问父类的方法该怎么办呢?在其他语言中,可以用 super.toString() (ES6 方式)
js 替的方法是在通过 call()或 apply()调用父类的原型对象的方法时传入一个子类的对象

function Rectangle(length, width) {
  this.length = length;
  this.width = width;
}
Rectangle.prototype.getArea = function() {
  return this.length * this.width;
};
Rectangle.prototype.toString = function() {
  return "[Rectangle " + this.length + "x" + this.height + "]";
};
// inherits from Rectangle

function Square(size) {
  Rectangle.call(this, size, size);
}
Square.prototype = Object.create(Rectangle.prototype, {
  constructor: {
    configurable: true,
    enumerable: true,
    value: Square,
    writable: true
  }
});

//调用超类型方法
Square.prototype.toString = function() {
  vartext = Rectangle.prototype.toString.call(this);
  return text.replace("Rectangle", "Square");
};

在这个版本的代码中,Square.prototype.toString()通过 call()调用 Rectangle.prototype.toString()。该方法只需在返回文本结果前用“Square”替换“Rectangle”。这种做法看上去可能有一点冗长,但这是唯一的访问父类方法的手段。

总结

1.JavaScript 通过原型对象链支持继承。当将一个对象的[[Prototype]]设置为另一个对象时,就在这两个对象之间创建了一条原型对象链。所有的泛用对象都自动继承自Object.prototype。如果你想要创建一个继承自其他对象的对象,你可以用 Object.create()指定[[Prototype]]为一个新对象。

2.可以在构造函数中创建原型对象链来完成自定义类型之间的继承。通过将构造函数的 prototype 属性设置为某一个对象,就建立了自定义类型对象和该对象的继承关系。构造函数的所有对象实例共享同一个原型对象,所以它们都继承自该对象。这个技术在继承其他对象的方法时工作得十分好,但你不能用原型对象继承自有属性

3.为了正确继承自有属性,可以使用构造函数窃取。只需以 call()或 apply()调用父类的构造函数,就可以在子类里完成各种初始化。结合构造函数窃取和原型对象链是 JavaScript 中最常见的继承手段。由于和基于类的继承相似,这个组合经常被称为伪类继承

4.可以通过直接访问父类原型对象的方式访问父类的方法。当你这么做时,你必须以 call()或 apply()执行父类方法并传入一个子类的对象。

上一篇下一篇

猜你喜欢

热点阅读