JavaScript面向对象系列:五、继承

原型对象链和 Object.prototype

js內建的继承方法被称为原型对象链,又可以称为原型对象继承。原型对象的属性可以经由对象实例访问,这就是继承的一种形式。对象实例继承了原型对象的属性。因为原型对象也是一个对象,他也有自己的原型对象并继承其属性。这就是原型对象链,而原型对象继承它的原型对象,以此类推。

所有对象,包括哪些你自己定义的对象都自动继承自 Object,除非另有指定。更确切的说,所有对象都继承自 Object.prototype。任何以队形字面量形式定义的对象,其 [[Prototype]] 的值都被设为 Object.prototype,这意味着它继承 Object.prototype 的属性。

var book = {
title:"javascript"
};

var prototype = Object.getPrototypeOf(book);
console.log(prototype === Object.prototype); //true

继承自 Object.peototype 的方法

前面说到的很多方法其实都是定义在 Object.prototype 上面的。因此可以被其他对象继承。

方法 作用
hasOwnProperty() 检查是否存在一个给定名字的自有属性
propertyIsEnumerable() 检查一个自有属性是否可枚举
isPrototypeof() 检查一个对象是否是另一个对象的原型对象
valueOf() 返回一个对象的值表达
toString() 返回一个对象的字符串表达

valueOf,每当一个操作符被用于一个对象时就会调用 valueOf() 方法。 valueOf() 默认返回对象实例本身。原始封装类型重写了 valueOf() 以使得它对 String 返回一个字符串,对 Boolean 返回一个布尔值,对 Number 返回一个数字。类似的,Date 对象返回一个 epoch 时间。如果你的对象也要这样使用操作符,也可以自定义 valueOf() 方法.定义的时候并没有改变操作符的行为,仅仅定了操作符默认行为所使用的值。

toString(),一旦 valueOf() 方法返回的是一个引用而不是原始值的时候,就会回退调用 toString() 方法。另外,当js期望一个字符串时,也会对原始值隐式调用 toString() .例如,当加号操作符的一边是一个字符串时,另一边会被自动转换成字符串。如果另一边是一个原始值,会自动被转换成一个字符串表达(例如,true 转换成 “true”)。如果另一边是一个引用值,则会调用 valueOf()。如果 valueOf() 返回一个引用值,则调用 toString()

例如

var book = {
title:"javascript"
};

var message = "Book = " + book;
console.log(message); //"Book = [object Object]"

这段以 “Book =” 和book来构造字符串。因为book是一个对象,此时调用它的 toString() 方法。该方法继承自 Object.prototype,大部分js引擎返回默认值 “[object Object]”。如果对这个值满意,就不需要改变对象的 toString() 方法。定义自己的 toString() 方法有时候可以为此类字符串转换提供更过信息的值。

var book = {
title:"javascript",
toString:function(){
return "[Book " + this.title + "]";
}
};

var message = "Book = " + book;
console.log(message); //"Book = [Book javascript]"

修改 Object.pototype

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

例如

Object.prototype.add = function(value){
return this + value;
}

var book = {
title:"javascript"
};

console.log(book.add(5)); //"[object Object]5";
console.log("title".add("end")); //"titleend"

//在浏览器中
console.log(document.add(true)); //"[object HTMLDocument]true"
console.log(window.add(5)); //"[object Window]6"

上面给 Object.prototype 添加方法可能会带来不可预知的结果

例如

var empty = {};

for(var prpperty in empty){
console.log(property);
}
// add

空对象依然会输出一个”add”属性。考虑到js中 for-in 使用频繁,为 Object.prototype 添加可枚举属性会影响大量代码。所以可以在 for-in 中使用 hasOwnProperty().

例如

var empty = {};

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

这样循环只会输出对象自有属性,不会输出原型属性。

对象继承

对象继承是最简单的继承类型。唯一需要做的就是制定那个对象是新对象的 [[Prototype]] 。对象字面量形式会隐式指定 Object.prototype 为其 [[Prototype]],也可以使用 Object.create() 方法显式指定。

例如

var book = {
title:"javascript"
};

//和下面是一样的

var book = Object.create(Object.prototype,{
title:{
configurable:true,
enumerable:true,
value:"javascript",
writable:true
}
});

继承其他对象

var person1 = {
name:"laowang",
sayName:function(){
console.log(this.name);
}
};

var person2 = Object.create(person1,{
name:{
configurable:true,
enumerable:true,
value:"xiaowang",
writable:true
}
});

person1.sayName(); //"laowang"
person2.sayName(); //"xiaowang"

console.log(person1.hasOwnProperty("sayName")); //true
console.log(person1.isPrototypeOf(person2)); //true
console.log(person2.hasOwnProperty("sayName")); //false

另外,也可以通过 Object.create() 创建 [[Prototype]] 为null的对象,这样的对象是没有原型对象链的对象。意味着 toString()valueOf() 等內建方法都不存在该对象上面。实际上,这种对象完全就是一个没有任何预定义属性的白板,也是一个完美的哈希容器,因为不会有自由属性和原型属性的冲突。

例如

var nakedObject = object.create(null);

console.log("toString" in nakedObject); //false
console.log("valueOf" in nakedObject); //false

构造函数继承

js中的对象继承也是构造函数继承的基础。几乎所有的函数都有 prototype 属性,它可以被修改或者换。该 prototype 属性被自动设置为一个新的继承自 Object.prototype 的泛用对象,该对象有一个自有属性 constructor ,实际上js引擎自动做了下面的事情

function YourConstructor(){

}

//js引擎自动为你做了下面的事情
YourConstructor.prototype = Object.create(Object.prototype,{
constructor:{
configurable:true,
enumerable:true,
value:YourConstructor,
writable:true
}
});

创建出来的对象都继承自 Object.prototype。YourConstructor 是Object的子类,Object是YourConstructor 的父类。

由于 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 + " ]";
};

//继承Reactangle
function Square(size){
this.length = size;
this.width = size;
}

Square.prototype = new Rectangle();
Square.prototype.constuctor = 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

console.log(square instanceof Square); //true
console.log(square instanceof Rectangle); //true
console.log(square instanceof Object); //true

此时不需要给Rectangle的调用提供参数,因为他们不需要被使用,而且如果提供了,那所有Square的对象实例都会共享同样的维度。用这种方式改变原型对象链时,需要确保构造函数不会再参数缺失时抛出错误(很多构造函数包含的初始化逻辑会需要参数)且构造函数不会改变任何全局状态,比如追踪有多少实例被创建等。

rect作为Rectangle的实例被创建,而square则是作为Square的实例被创建。两个对象都有getArea方法,那是因为继承自Reatangle.prototype。instanceof操作符认为变量square同时是Square、Rectangle、Object的实例,因为instanceof是使用原型对象链检查对象类型。

使用 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 + " ]";
};

//继承Reactangle
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
````

## 构造函数窃取

由于js中的继承是通过原型对象链来实现的,因此不需要调用对象的父类构造函数。如果需要在子类构造函数中调用父类构造函数,就需要用 ___call()___ 或者 ___apply()___。

例如
```javascript
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 + " ]";
};

//继承Reactangle
function Square(size){
Retangle.call(this,size,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 square = new Square(6);

console.log(square.length); //6
console.log(square.width); //6
console.log(square.getArea()); //36

由于这种做法模仿了那些基于类语言的类继承,通常被称为伪类继承。

访问父类方法

子类提供的新功能覆盖父类方法很常见,但是如果还想访问父类方法,只能通过 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 + " ]";
};

//继承Reactangle
function Square(size){
Retangle.call(this,size,size);
}

Square.prototype = Object.create(Rectangle.prototype.{
constructor:{
configurable:true,
enumerable:true,
value:Square,
writable:true
}
});
Square.prototype.toString = function(){
var text = Rectangle.prototype.toString().call(this);
return text.replace("Rectangle","Square");
};

总结

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

 

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

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

可以通过直接访问父类原型对象的方式访问父类方法。必须以 call() 或者 apply() 执行父类方法并传入一个子类的对象。

JavaScript面向对象系列:四、构造函数和原型对象

[[Prototype]] 属性

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

可以调用对象的 Object.getPrototypeOf() 方法读取[[Prototype]] 属性的值。

var obj = {};
var prototype = Object.getPrototypeOf(obj);
console.log(prototype === Object.prototype); //true

大部分js引擎都会提供一个 __ proto __属性。该属性使你可以直接读写 ___[[Prototype]]___ 属性。

可以使用 isPrototypeOf() 方法检查某个对象是否是另一个对象的原型对象,该方法在所有对象中都有.

var obj = {};
console.log(Object.prototype.isPrototypeOf(obj)); //true

因为obj是一个泛用对象,原型是Object.prototype

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

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

obj.toString = function(){
return "[object Custom]";
}
console.log(obj.toString()); //"[object Custom]"

delete obj.toString;
console.log(obj.toString()); //"[object Object]"

delete obj.toString;
console.log(obj.toString()); //"[object Object]"

上述例子也说明了,仅当自有属性被删除时,原型属性才会再一次被使用。delete操作符仅对自有属性起作用,无法删除原型对象的属性。

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

使用对象字面量形式改写原型对象时要注意 constructor 属性

例如

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

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

var person1 = new Person("laowang");

console.log(person1 instanceof Person); //true
console.log(person1.constructor === Person); //false
console.log(person1.constructor === Object); //true

使用这种方式创建原型对象,constructor 属性会有误差。每一个原型对象都有一个 constructor 属性,这是其他对象实例没有的。

当一个函数被创建时,它的 prototype 属性也被创建,且该原型对象的 constructor 属性指向该函数。当使用对象字面量形式改写原型对象的时候,constructor 被置为泛对象Object。得手动修正。

例如

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

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

var person1 = new Person("laowang");
var person2 = new Person("xiaowang");

console.log(person1 instanceof Person); //true
console.log(person1.constructor === Person); //true
console.log(person1.constructor === Object); //false

console.log(person2 instanceof Person); //true
console.log(person2.constructor === Person); //true
console.log(person2.constructor === Object); //false

构造函数、原型对象和对象实例之间的关系最有趣的一个方面也许就是对象实例和构造函数之间没有直接联系。不过对象实例和原型对象以及原型对象和构造函数之间都有直接联系。

改变原型对象

给定类型的所有对象实例功效一个原型对象,所以可以一次性扩充所有对象实例。_[[Prototype]] 属性只是包含了一个指向原型对象的指针。任何对原型对象的改变都立即反映到所有引用它的对象实例上。

例如

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

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

var person1 = new Person("laowang");
var person2 = new Person("xiaowang");

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

Person.prototype.sayHi = function(){
console.log("Hi");
};

person1.sayHi(); //"Hi"
person2.sayHi(); //"Hi"

对象封印和对象冻结只能操作对象的自有属性。封印或者冻结之后,无法添加自有属性或者改变冻结对象的自有属性。

例如

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

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

var person1 = new Person("laowang");
var person2 = new Person("xiaowang");

Object.freeze(person1);

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

Person.prototype.sayHi = function(){
console.log("Hi");
};

person1.sayHi(); //"Hi"
person2.sayHi(); //"Hi"

其实,[[Prototype]] 属性是对象实例的自有属性,属性本身被冻结,但是其指向的值(原型对象)并没有被冻结。

实际开发中,可能不会频繁的使用原型对象,但是理解对象实例及其原型对象之间的关系是非常重要的。

改变内建原型对象

所有的內建对象都有构造函数,因此也都有原型对象可以改变。

例如,简单修改Array的原型对象

Array.prototype.sum = function(){
return this.reduce(function(pre,cur){
return pre + cur;
});
};

var numbers = [1,2,3,4,5];
var result = numbers.sum();

console.log(result); //15

总结

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

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

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

內建对象也有可以被修改的原型对象。

我们为什么需要redux

1.1.1 redux是什么

通俗的来讲,redux就是一个state管理库

1.1.2 不使用redux构建应用

  1. 一般构建的React组件内部可能是一个完整的应用,它自己工作良好,你可以通过属性作为API控制它。但是更多的时候发现React根本无法让两个组件互相交流,使用对方的数据。然后这时候不通过DOM沟通(也就是React体制内)解决的唯一办法就是提升state,将state放到共有的父组件中来管理,再作为props分发回子组件。
  2. 子组件改变父组件state的办法只能是通过事件触发父组件声明好的回调,也就是父组件提前声明好函数或方法作为契约描述自己的state将如何变化,再将它同样作为属性交给子组件使用。这样就出现了一个模式:数据总是单向从顶层向下分发的,但是只有子组件回调在概念上可以回到state顶层影响数据。这样state一定程度上是响应式的。
  3. 为了面临所有可能的扩展问题,最容易想到的办法就是把所有state集中放到所有组件顶层,然后分发给所有组件。这样就造成了很多中间组件为了传递props而增加一些冗余的属性。
  4. 最重要的是父组件与子组件的通信,会造成数据的重复,带来的一个问题是如何保证数据重复的数据一致,如果数据存储多份而且不一致,就很难决定到底使用哪个数据作为正确结果了。
  5. 对于数据重复的问题,一致很直观的解决方法就是以某一个组件的状态为准,这个组件是状态的”领头羊”,其余组件都保持和”领头羊”的状态同步,但是实际情况下这种方法可能难以实施。
  6. 另一种思路是,干脆不要让任何一个react组件扮演”领头羊”角色,把数据源放在react组件之外形成全局状态。这便是redux的store,全局唯一的数据源。

1.1.3 redux为我们做了什么

image

1.1.4 按照redux思想来设计

redux真正的灵魂在其设计思想,很多时候我们可能并不需要redux库本身,我们可以尝试着来应用其思想.

例如:使用组件内state的计数器

import React, { Component } from 'react';

class Counter extends Component {
state = { value: 0 };

increment = () => {
this.setState(prevState => ({
value: prevState.value + 1
}));
};

decrement = () => {
this.setState(prevState => ({
value: prevState.value - 1
}));
};

render() {
return (
<div>{this.state.value}
<button>+</button>
<button>-</button></div>
)
}

应用redux思想的计数器

import React, { Component } from 'react';

const counter = (state = { value: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { value: state.value + 1 };
case 'DECREMENT':
return { value: state.value - 1 };
default:
return state;
}
}

class Counter extends Component {
state = counter(undefined, {});

dispatch(action) {
this.setState(prevState => counter(prevState, action));
}

increment = () => {
this.dispatch({ type: 'INCREMENT' });
};

decrement = () => {
this.dispatch({ type: 'DECREMENT' });
};

render() {
return (
<div>{this.state.value}
<button>+</button>
<button>-</button></div>
)
}
}

1.1.5 用redux的好处

  1. 前端开发state可控。
  2. 前端数据结构统一管理。
  3. 数据流向单一,团队开发互相影响较小。