# JavaScript 面向对象

Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言

如果我们要把"属性"(property)和"方法"(method),封装成一个对象,How?

# 原始封装

//声明一个Cat类型的Schema
var Cat = {
  name : '',
  color : ''
}

//根据原始的Schema 生成实例
var cat1 = {}; // 创建一个空对象
cat1.name = "大毛"; // 按照原型对象的属性赋值
cat1.color = "黄色";

var cat2 = {};
cat2.name = "二毛";
cat2.color = "黑色";

这就是最简单的封装了,把两个属性封装在一个对象里面。但是,这样的写法有两个缺点,一是如果多生成几个实例,写起来就非常麻烦;二是实例与原型之间,没有任何办法,可以看出有什么联系

# 创建对象

# 工厂模式

//工厂模式
function createPerson(name, age, job) {
  var o = {};  // var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function() {
    console.log(this.name)
  };
  return o;
}

var person1 = createPerson('yixing', 24, 'dev');
var person2 = createPerson('xiaoya', 24, 'net');

局限:工厂模式虽然解决了创建多个相似对象的问题,但是却没有解决对象识别的问题(即怎样知道一个对象的类型) person1和person2之间没有内在的联系,不能反映出它们是同一个原型对象的实例

# 构造函数模式

//构造函数模式
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {
    console.log(this.name)
  }
}

var person1 = new Person('yixing', 24, 'dev');
var person2 = new Person('xiaoya', 24, 'dev');

使用此种方式来调用构造函数会经历以下四个步骤:

  • 创建一个对象
  • 将构造函数的作用域赋值给新对象(因此this就指向了这个新对象)
  • 执行构造函数中的代码(为这个新对象添加属性)
  • 返回新对象

person1和person2各自带有一个constructor(构造函数属性)

构造函数也是函数,与普通函数的唯一区别就是调用方式不同。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;

优势:创建自定义的构造函数意味着可以将它的实例标识为一种特定的类型;这正是构造函数模式胜过工厂模式的地方 局限:每个方法都要在每个实例上重新创建一遍,这就意味着每个单独的实例维护着各自独立但是相同的方法。但是这样是完全没有必要的重复代码

# 原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途就是包含可以由特定类型的所有实例共享的属性和方法。 简单的理解就是通过prototype来调用原型对象。 使用原型对象的好处是可以让所以对象实例共享它所包含的属性和方法。 等于,不必要构造函数中定义对象实例的信息,而是可以将这些信息添加到原型对象中

//原型模式
function Person() {
  
}

Person.prototype.name = 'Yixing';
Person.prototype.age = '24';
Person.prototype.job = 'dev';
Person.prototype.sayName = function() {
  console.log(this.name)
};

var person5 = new Person();
person5.sayName(); //Yixing

var person6 = new Person();
person6.sayName(); //Yixing

console.log(person5.sayName == person6.sayName); //true

优势:与构造函数模式不同的是,新对象的这些属性和方法是由所有的实例所共享的

原型对象 无论什么时候,只要创建了一个新的函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。 在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。

如何理解:

//1.创建一个函数
function Person() {}

//2.给这个函数添加一个prototype属性,这个属性指向函数的原型对象
Person.prototype => 原型对象
:
原型对象{
  constructor: f(){}
}

Person.prototype的值等于原型对象
Person.prototype == {
  constructor: f(){}
}                                                                  

原型对象里面有一个constructor属性,这个属性又指向原先的函数。
Person.prototype.constructor === Person

当调用构造函数创建一个新实例后,该实例的内部包含一个指针(内部属性),指向构造函数的原型对象(而不是指向构造函数)。这个指针在ECMA-262第五版中定义这个指针叫做[[Prototype]],也没有定义标准的访问方式,但在浏览器的实现中,每个实例对象支持一个__proto__属性来访问。

Note: 这个指针连接的是实例构造函数的原型对象,而不是实例构造函数

如何理解:

1.创建一个实例
var person = new Person();

2.实例的__proto__指向构造函数的原型对象
person.__proto__ === Person.prototype

js-008

实例对象的属性 如果在实例中添加了一个属性,并且这个属性和实例原型中的一个属性重名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性

原型对象的局限

  • 局限一:省略了为构造函数传递初始化参数这一环节,结果所有的实例在默认情况下都将取得相同的属性值
  • 局限二:对于引用类型的值来说,通过原型对象实际上维护着一份共享代码,其中一个实例的更改会影响另一个,多数情况下,这是我们所不期望的。我们期待的是实例要有自己的全部属性

如何理解:

//原型模式的局限  另一种原型模式的写法
function Person() {}

Person.prototype = {
  constructor: Person,
  name: 'yixing',
  age: 24,
  job: 'dev',
  friends: ['a', 'b']
};

var person7 = new Person();
var person8 = new Person();

person7.friends.push('c');
console.log(person7);  //'a,b,c'
console.log(person8);  //'a,b,c'
console.log(person7.friends === person8.friends);  //true

# 组合使用构造函数模式和原型模式

构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性,这样,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省了内存。

这种构造函数与原型混成的模式,是目前ECMAScript中使用广泛,认同度最高的一种创建自定义类型的方法。

//组合使用构造函数模式和原型模式
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ['a', 'b']
}

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

var person9 = new Person('yixing', 24, 'dev');
var person10 = new Person('xiaoya', 24, 'net');

person9.friends.push('c');

console.log(person9);  //'a,b,c'
console.log(person10);  //'a,b'
console.log(person9.friends === person10.friends);  //false
console.log(person9.sayName === person10.sayName);  //true

# 寄生构造函数模式

# 稳妥构造函数模式

# 继承

继承是面向对象语言中的一个最为人津津乐道的概念,许多面向对象语言都支持两种继承方式,接口继承和实现继承

  • 接口继承只继承方法签名
  • 实现继承则继承实际的方法

由于函数没有签名,在ECMAScript中无法实现接口继承。所以ECMAScript只支持实现继承,主要是依靠原型链来实现的

# 原型链

基本思想:利用原型对象让一个引用类型继承另一个引用类型的属性和方法

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象内部的指针。假如我们让原型对象等于另一个类型的实例,那么此时的原型就包含一个指向另一个原型的指针,这样子层层递进,就构成了实例和原型的链条。

如何理解:

//原型链
function SuperType() {
  this.superName = 'super';
}

SuperType.prototype.getSuperName = function() {
  return this.superName;
};

function SubType() {
  this.subName = 'sub';
}

//继承了SuperType
SubType.prototype = Object.create(SuperType.prototype);  //合理推荐的 调用Object.create会创建一个新的对象并把内部的[[Prototype]]关联到你指定的对象

SubType.prototype = new SuperType();   //基本满足需求,但是会产生一些副作用,例如在SuperType的构造函数中有一些额外的操作,就会影响到SubType本身


SubType.prototype = SuperType.prototype; // 并不会创建一个关联到SubType.prototype的新对象,它只是让SubType.prototype直接引用了SuperType.prototype对象,这样子在执行SubType.prototype.getName 会直接修改SuperType.prototype本身,这是我们不希望看到的

SubType.prototype.getSubName = function() {
  return this.subName;
};

var ins = new SubType();
console.log(ins.getSuperName()); //'super'

js-009

实现原型链的本质就是重写原型对象,替换为一个新类型的实例

注意:

  1. 给原型添加方法一定要放在替换原型的语句之后

  2. 在通过原型链实现继承时,不能使用对象字面量创建原型方法,这样子就会复写原型链,使得继承无效 即:

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

原型链的缺陷

    1. 在通过原型链实现继承时,原型对象成为了父类的实例,这样,父类的实例属性就默认的成为了子类原型的属性,这是我们不期待的。
    1. 无法向父类的构造函数传递参数

# 借用构造函数

为了解决原型链技术的缺陷:即原型中包含引用类型的值,可以使用一种叫做借用构造函数(伪造对象/经典继承)

思路:在子类的构造函数中调用超类型的构造函数

//借用构造函数
function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue']
}

function SubType() {
  //继承了SuperType
  SuperType.call(this, 'yixing');
}

var ins1 = new SubType();
ins1.colors.push('green');
console.log(ins1.colors);  //'red,blue,green'

var ins2 = new SubType();
console.log(ins2.colors);  //'red,blue'

这样子在创建SubType实例时调用了SuperType的构造函数

借用构造函数缺陷

  • 与构造函数模式相似,方法都在构造函数中定义,无法做到函数复用
  • 父类的原型中定义的方法,对子类是不可见的

# 组合继承(伪经典继承)

将原型链和借用构造函数一起使用的方式。

思路:使用原型链实现对原型属性的和方法的继承,通过借用构造函数来实现对实例属性的继承

//组合继承
function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue']
}

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

function SubType(name, age) {
  //继承属性  //第二次调用
  SuperType.call(this, name);
  this.age = age;
}

//继承方法
SubType.prototype = new SuperType();  //第一次调用
SubType.prototype.constructor = SubType;  //如果不添加,原型(父类的实例)上是没有这个属性的
SubType.prototype.sayAge = function() {
  console.log(this.age)
};

var ins3 = new SubType('yixing', 24);
ins3.colors.push('green');
console.log(ins3.colors);  //'red,blue,green'
ins3.sayName();  //'yixing'
ins3.sayAge();  //24

var ins4 = new SubType('xiaoya', 23);
ins4.colors.push('yellow');
ins4.sayName();  //'xiaoya'
ins4.sayAge();  //23

组合继承的缺陷

无论什么情况下,都会调用两次父类型的构造函数:一次是在创建子类型的原型时,另一次是在子类型的构造函数内部。 这会导致在第一次调用时,会将父类的所有的实例属性挂载到子类的原型上;第二次调用在新对象上创建了自己的新的实例属性,实现了覆盖的效果

Note: 第二次调用之后,子类原型上的父类实例属性依旧存在

# 原型式继承

# 寄生式继承

# 寄生组合式继承

//寄生组合式继承
function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue']
}

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

function SubType(name, age) {
  //继承属性  //第二次调用
  SuperType.call(this, name);
  this.age = age;
}

//继承方法
SubType.prototype = Object.create(SuperType.prototype);  //第一次调用
SubType.prototype.constructor = SubType;  //如果不添加,原型(父类的实例)上是没有这个属性的
SubType.prototype.sayAge = function() {
  console.log(this.age)
};

var ins3 = new SubType('yixing', 24);
ins3.colors.push('green');
console.log(ins3.colors);  //'red,blue,green'
ins3.sayName();  //'yixing'
ins3.sayAge();  //24

var ins4 = new SubType('xiaoya', 23);
ins4.colors.push('yellow');
ins4.sayName();  //'xiaoya'
ins4.sayAge();  //23

js-010

陕ICP备20004732号-3