JavaScript 面向对象精要 - Nicholas C.Zakas(二)

avatarplhDigital nomad
看一本js书好不好,主要看他对于面向对象的描述以及原型继承的描述。

第五章 继承

如何学习创建对象时理解面向对象js编程的第一步,而第二部是理解继承。

5.1 原型对象链和Object.prototype

JavaScript内建的继承方法被称之为原型对象链,又称之为原型对象继承。如果上一章所看,原型对象的属性可经由对象实例访问,这就是继承的一种形式。对象实例继承了原型对象的属性。因为原型对象也是一个对象。他也有自己的原型并继承其属性。因此可以说所有对象都继承自Object。

5.1.1 继承自 Object.prototype 的方法

前几章里用到的多个方法其实都是定义在Object.prototype上的。因此可以被其他对象继承,这些方法如下。

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

这5种方法经由继承出现在所有对象种。当需要让对象在JavaScript中以一致的方式工作,最后尤为重要,有时候你甚至会想要自己定义他们。

1. valueOf

返回原本的值。当每一个操作符被用于一个对象时候就会调用valueOf的方法。默认返回对象实例本身。。原始封装类型重写valueOf,对于字符串返回字符串本身,对于Boolean返回一个布尔值。对number返回一个数字。

var now = new Date();
var earlier = new Date(2010, 1, 1);

console.log(now > earlier);

上面这个例子,now是一个代表当前时间的Date,而earlier是一个过去的时间,当使用<比较的时候,在两个对象都调用了valueOf()的方法,另外,你甚至可以对比两个Date相减来获得他们在epoch时间上的差值。

2.toString()

一旦value()返回的是一个引用值而不是原始值的时候,就会回退调用toString()方法,另外,当JavaScript期待一个字符串的时候,也会对原始值隐式调用toString()。例如,当加号操作符的一边是一个字符串,另一边会被自动转化成字符串。如果另一边是一个原始值,就会自动转化成字符串。不懂请看下面例子,重写Object的toString原型方法,'name'+{}; // namename ,字符串和布尔值相加会先将右边的布尔值转化成字符串,通过toString()这个方法。,首先如果右边是引用值,会先调用value的方法,如果value返回的还是一个引用值,那就调用toString() image image

const str = {
  name: 'peng',
  toString() {
    return 'toString';
  },
  valueOf() {
    return 'val';
  },
};
Object.prototype.valueOf = function (){
  return 'value'
}
Object.prototype.toString = function (){
  return 'staring'
}
console.log(`name${str}`);    // namestring

修改Object.prototype

修改Object会影响所有对象,这很危险。

Object.prototype.add = function (num) {
  return this + num;
};
console.log({}.add(5));   // [object Object]5

这个新添加的属性是可枚举的.请看下面的例子,我给原型对象添加add方法,返回this+val,,这个会返回本对象并且因为他是引用值,所以会调用toString()方法,而toString方法又被我改写了,所以返回的就是'to stirng5'。 同时被新添加的add方法是实体字,说明他是可以被枚举的。

image

image

对象继承

对象继承是最简单的原型继承,你唯一需要做的就是指定新对象的[[Prototype]]指向原型对象。 同时可以使用Object.create()方法指定,他接受2个参数,第一个是需要被设置成新对象的[[Prototype]]的对象,,第二个参数和Object.definedProperties()中使用的一样。

var book = {
  title: 'your now Principles of Object-Oriented Javascript.'
}
===========**the same as below**============
var book = Object.create(Object.prototype, {
    title: {
        configurable: true,
        enumerable: true,
        value: 'The Principles of Object-Oriented Javascript',
        writable: true,
    }
});

两种声明具有相同效果,第一种使用了字面量自定义属性,而第二种使用了Object.create,显示用了同样的操作,这种默认继承无趣,但是如果你继承其他对象就有意思了。

const person1 = {
  name: 'peng',
  sayName() {
    console.log(this.name);
  },
};

const person2 = Object.create(person1, {
  name: {
    configurable: true,
    enumerable: true,
    value: 'Greg',
    writable: true,
  },
});

person1.sayName();    // 'peng'
person2.sayName();    // 'Grey'
console.log(person1.hasOwnProperty('sayName');    // true
console.log(person2.hasOwnProperty('sayName');    // false
console.log(person1.isPrototypeOf(person2);    // true

person2继承person1 继承Object

假设你通过Object.create()创建时第一个参数为null,那么继承指向空。看下图,他没有任何方法,因此同时,他也是一个完美的hash容器,因为你给他命名任何方法,都可以,不存在任何冲突。 image image

无法转换,因为不存在toString方法来进行隐式转换。一个很有意思,你可以通过它创建一个没有原型的对象。 image

构造函数继承

当你声明一个类的时候,JavaScript引擎默认帮你做了如下事情,

function Person(){}
Person.prototype = Object.create(Object.prototype, {
  constructor: {
    configurable: true,
    enumerable: true,
    value: Person,
    writable: true,
  }
})

image

上面可以看出,你不需要做任何事情,这段代码帮你构造一个构造函数,其原型指向另一个继承自Function的的对象,并且它有一个新属性constructor,其值指向构造函数,如此循环下去,但是他们是一种循环式的自引用,众所周知,原型链仅仅只是一个指向对象的指针。这只是一种自己指向自己的循环引用point。下图例子说的很清楚,我这里只有一个a对象,a对象属性a指向它本身。那这就是循环自引用。而构造函数仅仅只是这样一个例子的复杂化而已。所有引用类型都是指针指向(point) image

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

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

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

// inherits from Rectangle
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}]`;
};

const rect = new Rectangle(5, 10);
const square = new Square(6);

console.log(rect.getArae());
console.log(square.getArae());

console.log(rect.toString());
console.log(square.toString());

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

这个代码有两个构造函数:Rectangle和Square。Suqare构造函数的prototype属性被改写成Rectangle的一个对象实例。此时不需要给Rectangle的调用提供参数,因为他们不需要被使用,而且如果提供了,那么所有Square的对象实例都会享有共同的维度,用这种方法改变原型链,你需要确保构造函数不会再参数却是的时候抛出错误。(很多构造函数包含的初始化逻辑会需要参数)且构造函数不会改变任何全局状态。Square.prototype被改写后,constructor属性被重置为Square。 然后,rect作为Rectangle的实例对象呗创建,而square则被作为Square的实例创建。两个对象都有getArea()方法,因为他被继承自Rectangle.prototype和Object的对象实例。instanceof使用原型对象链检查对象类型。 Square.prototype并不需要真的被改写为一个Rectangle对象,毕竟Rectangle构造函数并没有真的为Square做什么必要的事情,事实上,唯一相关的部分是Square.prototype需要指向Rectangle.prototype,使得继承得以实现,这意味着你可以用Object.create()简化例子,代码如下

// inherits from Rectangle
function Square(size){
  this.length=  size;
  this.width=  size;
}
Square.prototype = Object.create(Rectangle.prototype, {
  constructor: {
    configurable: true,
    enumerable: true,
    value: Square,
    writable: treu
  }
});
Square.prototype.toString = function() {
  return `[Square ${this.length}x${this.width}]`;
};

在这个版本的代码中,Square.prototype被改写成一个新的继承自Rectangle.prototype的对象,而Rectangle构造函数没有被调用。这意味着,你不再需要担心不参加构造函数会导致的错误。除此之外。

构造函数的窃取

啥意思?

由于JavaScript的继承通过原型对象继承来实现,因此不需要调用对象的父类的构造函数,如果你却是需要再子类构造函数中调用父类构造函数,那你就需要利用JavaScript函数工作的特性。 第二章提到过,call和apply方法允许你再调用函数是提供不同的this值,那正好是构造函数窃取的关键。而这个就是构造函数窃取的关键,只需要再子类构造函数中调用call或者apply调用父类的构造函数,并将新的创建的对象传进去即可。实际上,就是用自己的对象窃取父类的构造函数,如下例子。


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

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

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

// inherits from Rectangle
function Square(size) {
  Rectangle.call(this, size, size);

  // optional: add new prototype or override existing ones here
}

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}]`;
};

const square = new Square(6);

console.log(square.getArae());
console.log(square.width);
console.log(square.length);

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

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

5。5 访问父类方法

前面例子中,Square类型有自己的toString方法,,子类覆盖父类,但是如果你要访问父类怎么办???代替方法是在通过call或apply调用父类的原型对象的方法时传入一个子类的对象。

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

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

Rectangle.prototype.toString = function () {
  return `[Rectangle ${this.length}x${this.width}]`;
};
// inherits from Rectangle
function Square(size) {
  Rectangle.call(this, size, size);

  // optional: add new prototype or override existing ones here
}
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"。这种做法看起来有一点冗长,但是唯一有效访问父类的方法。

第六章 对象模式

终于讲到这一节,非常经典的啊。该书也只生写最后10页。精华就在后面,坚持看下去吧。

本章讲述创建高可管理和创建对象的能力。

6.1 私有成员和特权对象

  • 模块模式,俗称IIFE返回一个对象,而变量外部不可见,
var yourObject = (function (){
 const private = 'pengliheng'

 return {
   a:1,
   b:2
 }
}());

console.log(yourObject);

上面创建了匿名函数立即执行,。同时这意味着,这个函数仅存在于被调用瞬间,一旦执行就立即销毁了。IIFE常见的用于浏览器端环境打包模式。适用于模块化。

6.1.2 构造函数的私有成员

模块定义单个对象的是由属性上十分有效i,但对于那些同样需要私有属性的自定义类型又要怎么做?你可以在构造函数内部使用类似模块来创建每个实例的私有数据。如下例子:


function Person(name) {
  // define a variable only accessible inside of the Person constructor
  let age = 25;
  this.name = name;
  this.getAge = function () {
    return age;
  };
  this.growOlder = function () {
    age += 1;
  };
}

const person = new Person('Nicholas');

console.log(person.name);
console.log(person.getAge());

person.age = 100;
console.log(person.getAge());

person.growOlder();
console.log(person.getAge());
console.log(person);

上面代码中Person构造函数就有一个本地变量age。该变量被用于getAge()和growOlder()方法。当你创建Person的一个实例时候,该实例接受其自身的age变量,getAge()方法和growOlder()方法。这种做法很多时候都有类似模块模式,构造函数创建一个本地作用域返回this对象。上一章讨论过,将对象直接放在对象实例上不如放在其原型上面有效,如果你需要实例私有数据,这是唯一有效方法。 但是如果你需要所有实例都可以共享私有数据,就好像它被定义在原型上面那样,可以结合模块模式和构造函数,如下。

const Person = (function () {
  // define a variable only accessible inside of the Person constructor
  let age = 25;

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

  InnerPerson.prototype.getAge = function () {
    return age;
  };

  InnerPerson.prototype.growOlder = function () {
    age += 1;
  };
  return InnerPerson;
}());

const person = new Person('Nicholas');

console.log(person.name);
console.log(person.getAge());

person.age = 100;
console.log(person.getAge());

person.growOlder();
console.log(person.getAge());
console.log(person);

image 上面代码InnerPerson构造函数被定义在IIFE中。变量age被定义在构造函数外,但是在模块内部,并被两个原型对象的方法使用。IIFE返回InnerPerson构造函数作为全局作用域里面的Person构造函数使用,最终Person实例全部共享age作为闭包内部变量。

6.2 混入

JavaScript大量使用了伪类继承和原型对象继承,还有另一种就是混入。第一个对象接收者,通过直接赋值第二个对象提供者的属性从而接受了这些属性。

funtion mixin(receiver, supplier) {
  for(var property in supplier){
    if(supplier.hasOwnPrototype(property){
      receiver[property] = supplier[property];
    }
  }
}

函数mixin()接受2个参数,接收者和提供者,,通过枚举方式,将所有可枚举的属性赋值给接收者,通过for...in循环所有可枚举属性。

作用域安全的构造函数

涨见识。。

如果不通过new就创建实例函数

var a  =Array();

image

上图为什么呢,因为它上了安全套

function Person(name){
  if(this instanceof Person){
    this.name = name;
  } else {
    return new Person(name);
  }
}

对于上面这个构造函数,当自己呗new调用的时候,就设置name属性,如果不被new调用的时候,则以new递归调用自己来为自己创建正确的属性。这么做就能保证行为一致性。

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

console.log(person1 instanceof Person);   // true
console.log(person2 instanceof Person);   // true
全书完。这是我写的最细的一本书了,短短92页,我每一页都给照抄下来了😭。

reference:JavaScript面向对象精要