Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
plh97 opened this issue Apr 20, 2018 · 0 comments
Open

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

plh97 opened this issue Apr 20, 2018 · 0 comments
Assignees
Labels
javaScript 关于js的一些事 博客 写一些前端技术记录 学习 如果不学习,那今天和昨天又有什么区别 看书 其实如果不看书的话,那么每天写的东西都和昨天一样,又有什么意思

Comments

@plh97
Copy link
Owner

plh97 commented Apr 20, 2018

看一本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面向对象精要

@plh97 plh97 added 学习 如果不学习,那今天和昨天又有什么区别 博客 写一些前端技术记录 javaScript 关于js的一些事 看书 其实如果不看书的话,那么每天写的东西都和昨天一样,又有什么意思 labels Apr 20, 2018
@plh97 plh97 self-assigned this Apr 20, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
javaScript 关于js的一些事 博客 写一些前端技术记录 学习 如果不学习,那今天和昨天又有什么区别 看书 其实如果不看书的话,那么每天写的东西都和昨天一样,又有什么意思
Projects
None yet
Development

No branches or pull requests

1 participant