Live2D 看板娘 / Demo

了解面向对象编程

1、对象是什么

面向对象编程(Object oriented Programming,缩写为OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。

每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。对象可以复用,通过继承机制还可以定制。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。

(1)对象是单个实物的抽象

一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

(2)对象是一个容器,封装了属性(property)和方法(method)

属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。

2、构造函数

面向对象编程的第一步,就是要生成对象。前面说过,对象是是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。

典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。

JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。

构造函数就是一个普通的函数,但是有自己的特征和用法。

var Cat = function(){
    this.name = '小咪';
}

上面代码中, Cat 就是构造函数。为了与普通函数区别,构造函数名字的第一个字母通常大写。

构造函数的特点有两个:

  • 函数体内使用了 this 关键字,代表了所要生成的对象实例。
  • 生成对象的时候,必须使用new命令

根据需要,构造函数可以接受参数

function Cat (name){
    this.name = name;
}
var d1 = new Dog('小咪');
console.log(d1.name);//小咪

如果忘记使用new操作符,则this将代表全局对象window

function Cat(){
    this.name = name;
}
var d1 = Cat();
//Uncaught TypeError: Cannot read property 'name' of undefined
console.log(d1.name);

上述代码,忘记使用new命令,其实是导致d1编程了undefined,而name属性变成了全局变量。因此,应该非常小心,避免不使用new命令、直接调用构造函数

为了保证构造函数必须与new命令一起使用,一个解决办法是,构造函数内部使用严格模式,即第一行加上use strict。这样的话,一旦忘了使用new命令,直接调用构造函数就会报错。

function Cat(name){
    'use strict';
    this.name = name;
}
var d1 = Cat('小咪');

上面代码的Cat为构造函数,use strict命令保证了该函数在严格模式下运行。由于严格模式中,函数内部的this不能指向全局对象,默认等于undefined,导致不加new调用会报错(JavaScript不允许对undefined添加属性)。

instanceof

​ 该运算符运行时指出对象是否是特定类的一个实例

​ 另一个解决办法,构造函数内部判断是否使用了new命令,如果发现没有使用,则直接返回一个实例对象

instanceof操作符可以用来鉴别对象的类型

function Cat(name){
    if(!(this instanceof Dog)){
        return new Dog(name);
    }
    this.name = name;
}
var d1 = Cat('小咪');
console.log(d1.name);//'小咪'
console.log(Cat('小咪').name);//'小咪'

上述代码中的构造函数,不管加不加new命令,都会得到同样的结果

new命令

大家也能看到,如果我们想创建一个对象,声明构造函数之后,必须使用new命令来实例化对象。那么我们来研究一下new命令的原理

使用new命令时,它后面的函数依次执行下面的步骤

  1. 创建一个空对象,作为将要返回的对象实例
  2. 将这个空对象的原型,指向了构造函数的prototype属性
  3. 将这个空对象赋值给函数内部的this关键字
  4. 开始执行构造函数内部的代码

也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。

onstructor

​每个对象在创建时都自动拥有一个构造函数属性contructor,其中包含了一个指向其构造函数的引用。而这个constructor属性实际上继承自原型对象,而constructor也是原型对象唯一的自有属性。

function Cat(){
    
}
var d1 = new Cat();
console.log(d1.constructor === Cat);//true
console.log(d1.__proto__.constructor === Cat);//true

 

返回值

函数中的return语句用来返回函数调用后的返回值,而new构造函数的返回值有点特殊。

​如果构造函数使用return语句但没有指定返回值,或者返回值是一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果

function Fn(){
    this.a = 2;
    return;
}
var test = new Fn();
console.log(test);//{a:2}

​如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象

var obj = {a:1};
function fn(){
    this.a = 2;
    return obj;
}
var test = new fn();
console.log(test);//{a:1}

使用构造函数的好处在于所有用同一个构造函数创建的对象都具有同样的属性和方法

function Person(name){
    this.name = name;
    this.sayName = function(){
        console.log(this.name);
    }
}
var p1 = new Person('Tom');
var p2 = new Person('Jack');
​构造函数允许给对象来配置同样的属性,但是构造函数并没有消除代码冗余。使用构造函数的主要问题是每个方法都要在每个实例上重新创建一遍。在上面的例子中,每一个对象都有自己的sayName()方法。这也意味着如果有100个对象实例,就有100个函数做相同的事情,只是使用的数据不同。
function Person(name){
    this.name = name;
    this.sayName = function(){
        console.log(this.name);
    }
}
var p1 = new Person('Tom');
var p2 = new Person('Jack');
console.log(p1.sayName === p2.sayName);//false

​上面代码中,p1p2是用一个构造函数的两个实例,他们具有sayName方法。由于sayName方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每创建一个实例,就会新建一个sayName方法。这既没有必要,又浪费系统资源,因此所有sayName方法都是同样的行为,完全应该共享。

这个问题的解决方法。就是JavaScript的原型对象(prototype)

3、原型对象

​ 说起原型对象,就要说到原型对象实例对象构造函数的三角关系

​ 接下来以下面两行代码,来详细说明他们的关系

function Foo(){};
var f1 = new Foo();

构造函数

​用来初始化新创建的对象的函数是构造函数。在例子中,Foo函数是构造函数

实例对象

​通过构造函数的new操作创建的对象是实例对象,又通常被称为对象实例。可以用一个构造函数,构造多个实例对象。下面的f1f2就是实例对象

function Foo(){};
var f1 = new Foo();
var f2 = new Foo();
console.log(f1 === f2);//false

原型对象和prototype

​通过构造函数的new操作创建实例对象后,会自动为构造函数创建prototype属性,该属性指向实例对象的原型对象。通过同一个构造函数实例化的多个对象具有相同的原型对象。这个的例子中,Foo.prototype是原型对象

function Foo(){};
Foo.prototype.a = 1;
var f1 = new Foo;
var f2 = new Foo;

console.log(Foo.prototype.a);//1
console. log(f1.a);//1
console.log(f2.a);//1

prototype属性的作用

​JavaScript继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。

如何为对象指定原型。JavaScript规定,每个函数都有一个prototype属性,指向了一个对象

function fn(){};
//函数fn默认具有prototype属性,指向了一个对象
console.log(typeof fn.prototype);//"Object"

对于普通函数来说,该属性基本没用。但是对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。

function  Person(name){
    this.name = name;
}
Person.prototype.age = 16;
var p1 = new Person('大明');
var p2 = new Person('二明');
console.log(p1.age);//16
console.log(p2.age);//16

上面代码中,构造函数Personprototype属性,就是实例对象p1p2的原型对象。原型对象上添加一个age属性,结果,实例对象都共享了该属性。

原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上

Person.prototype.age = 40;
console.log(p1.age);//40
console.log(p2.age);//40

上面代码中,原型对象的age属性的值变为40,两个实例对象的age属性立刻跟着变了。这是因为实例对象其实没有age属性,都是读取原型对象的age属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。

如果实例对象就有某个属性或方法,它就不会再去原型对象寻找这个属性和方法

p1.age = 35;
console.log(p1.age);//35
console.log(p2.age);//40
console.log(Person.prototype.age) //40

上面代码中,实例对象p1age属性改为35,就使得它不再去原型对象读取age属性,后者的值依然为40

总结

​原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象

Person.prototype.sayAge = function(){
    console.log('My age is'+ this.age);
}

上面代码中,Person.prototype对象上面定义了一个sayAge方法,这个方法将可以在所有Person实例对象上面调用。

4、原型链

​JavaScript规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其它对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个原型链(prototype chain):对象的原型,再到原型的原型......

​如果一层层的往上寻找,所有对象的原型最终都可以寻找到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。这就是所有对象都有valueoftoString方法的原因,因为这是从Object.prototype继承的。

​那么,Object.prototype对象有没有它的原型呢?回答是Object.prototype的原型是nullnull没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null

Object.getPrototypeOf(Object.prototype);//null

上面代码表示,Object.prototype对象的原型是null,由于null没有任何属性,所以原型链到此为止。Object.getPrototypeOf方法返回参数对象的原型,具体介绍在对象的方法这节课中。

​读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)

注意,一级级向上,在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。

​举例来说,如果让构造函数prototype属性指向一个数组,就意味着实例对象可以调用数组方法

var MyArray = function () {};

MyArray.prototype = Array.prototype;
MyArray.prototype.constructor = MyArray;

var mine = new MyArray();
mine.push(1, 2, 3);
mine.length // 3
mine instanceof Array // true

constructor

​原型对象默认只会取得constructor属性,指向该原型对象对应的构造函数。至于其他方法,则是从Object继承来的

function Foo(){};
console.log(Foo.prototype.constructor === Foo);//true

由于实例对象可以继承原型对象的属性,所以实例对象也拥有constructor属性,同样指向原型对象对应的构造函数

function Foo(){};
var f1 = new Foo();
console.log(f1.constructor === Foo);//true
console.log(f1.constuctor === Foo.prototype.constructor);//true
f1.hasOwnProperty('constructor');//false

上面代码中,f1是构造函数Foo的实例对象,但是f1自身没有constructor属性,该属性其实是读取原型链上面的Foo.prototype.constructor属性

constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。

function Foo(){};
var f1 = new Foo();
console.log(f1.constructor === Foo);//true
console.log(f1.constructor === Array);//false

constructor属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor属性,防止引用的时候出错

function Person(name){
    this.name = name;
}
console.log(Person.prototype.constructor === Person);//true

//修改原型对象
Person.prototype = {
    fn:function(){
        
    }
};
console.log(Person.prototype.constructor === Person);//false
console.log(Person.prototype.constructor === Object);//true

​所以,修改原型对象时,一般要同时修改constructor属性的指向

function Person(name){
    this.name = name;
}
console.log(Person.prototype.constructor === Person);//true

//修改原型对象
Person.prototype = {
    constructor:Person,
    fn:function(){
        console.log(this.name);
    }
};
var p1 = new Person('阿黄');
console.log(p1 instanceof Person);//true
console.log(Person.constructor == Person);//false
console.log(Person.constructor === Object);//false

__proto__

​实例对象内部包含一个__proto__属性,指向该实例对象对应的原型对象

function Foo(){};
var f1 = new Foo;
console.log(f1.__proto__ === Foo.prototype);//true

总结

​构造函数、原型对象和实例对象之间的关系是实例对象和构造函数之间没有直接联系

function Foo(){};
var f1 = new Foo();

​以上代码的原型对象是Foo.prototype,实例对象是f1,构造函数是Foo

​原型对象和实例对象的关系

console.log(Foo.prototype === f1.__proto__);//true

​原型对象和构造对象的关系

console.log(Foo.prototype.constructor === Foo);//true

而实例对象和构造函数则没有直接关系,间接关系是实例对象可以继承原型对象的constructor属性

console.log(f1.constructor === Foo);//true

如果非要扯实例对象和构造函数的关系,那只能是下面这句代码,实例对象是构造函数的new操作的结果

var f1 = new Foo;

这句代码执行以后,如果重置原型对象,则会打破它们三个的关系

function Foo(){};
var f1 = new Foo;
console.log(Foo.prototype === f1.__proto__);//true
console.log(Foo.prototype.constructor === Foo);//true

Foo.prototype = {};
console.log(Foo.prototype === f1.__proto__);//false
console.log(Foo.prototype.constructor === Foo);//false

5、创建对象的5种模式

对象字面量

​一般地,我们创建一个对象会使用对象字面量的形式

(1)new构造函数

​ 使用new操作符后跟Object构造函数用以初始化一个新创建的对象

var person = new Object();
person.name = 'mjj';
person.age = 28;

(2)对象字面量

​ JavaScript提供了字面量的快捷方式,用于创建大多数原生对象值。使用字面量只是隐藏了与new操作符相同的基本过程,于是也可以叫做语法糖

var person = {
    name:'mjj';
    age:28
}

使用对象字面量的方法来定义对象,属性名会自动转成字符串

(3)Object.create()

生成实例对象的常用方法是,使用new命令让构造函数返回一个实例。但是很多时候,只能拿到一个实例对象,它可能根本不是由构造函数生成的,那么能不能从一个实例对象,生成另一个实例对象呢?

​ES5定义了一个名为Object.create()的方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。

//原型对象
var A = {
    getX:function(){
        console.log('hello');
    }
};
//实例对象
var B = Object.create(A);
console.log(B.getX);//"hello"

上面代码中,Object.create方法以A对象为原型,生成了B对象。B继承了A的所有属性和方法。

var person1 = {
    name:'mjj',
    age:28,
    sayName: function(){
        alert(this.name);
    }
}

如果我们要创建大量的对象,则如下所示

var person1 = {
    name:'mjj',
    age:28,
    sayName: function(){
        alert(this.name);
    }
}
var person2 = {
    name:'alex',
    age:38,
    sayName: function(){
        alert(this.name);
    }
}
/*
var person3 = {}
var person4 = {}
var person5 = {}
......
*/

虽然对象字面量可以用来创建单个对象,但如果要创建多个对象,会产生大量的重复代码

工厂模式

为了解决上述问题,人们开始使用工厂模式。该模式抽象了创建具体对象的过程,用函数来封装以特地接口创建对象的细节

function createPerson(name,age){
    var p = new Object();
    p.name = name;
    p.age = age;
    p.sayName = function(){
        alert(this.name);
    }
    return p;
}
var p1 = createPerson('mjj',28);
var p2 = createPerson('alex',28);
var p3 = createPerson('阿黄',8);

工厂模式虽然解决了创建多个相似对象的问题,但没有解决对象识别的问题,因为使用该模式并没有给出对象的类型

构造函数模式

可以通过创建自定义的构造函数,来定义自定义对象类型的属性和方法。创建自定义的构造函数意味着可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。该模式没有显式地创建对象,直接将属性和方法赋给了this对象,且没有return语句

function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayName = function(){
        alert(this.name);
    };
}
var person1 = new Person("mjj",28);
var person2 = new Person("alex",25);

使用构造函数的主要问题是每个方法都要在每个实例上重新创建一遍,创建多个完成相同任务的方法完全没有必要,浪费内存空间

function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayName = function(){
        alert(this.name);
    };
}
var p1 = new Person("mjj",28);
var p2 = new Person("alex",25);
//具有相同的sayName()方法在p1和p2这两个实例中缺占用了不同的内存空间
console.log(person1.sayName === person2.sayName);//false
构造函数拓展模式

​在构造函数模式的基础上,把方法定义转移到构造函数外部,可以解决方法被重复创建的问题

function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
function sayName(){
    alert(this.name);
}
var p1 = new Person("mjj",28);
var p2 = new Person("alex",25);
console.log(person1.sayName === person2.sayName);//true

现在,新问题又来了。在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而且,如果对象需要定义很多方法,就要定义很多全局函数,严重污染全局空间,这个自定义的引用类型没有封装性可言了。

寄生构造函数模式

该模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。该模式是工厂模式和构造函数模式的结合

寄生构造函数模式与构造函数模式有相同的问题,每个方法都要在每个实例上重新创建一遍,创建多个完成相同任务的方法完全没有必要,浪费内存空间

function Person(name,age){
    var p = new Object();
    p.name = name;
    p.age = age;
    p.sayName = function(){
        alert(this.name);
    }
    return p;
}
var p1 = new Person('mjj',28);
var p2 = new Person('alex',28);
//具有相同作用的sayName()方法在person1和person2这两个实例中却占用了不同的内存空间
console.log(p1.sayName === p2.sayName);//false

还有一个问题是,使用该模式返回的对象与构造函数之间没有关系。因此,使用instanceof运算符和prototype属性都没有意义。所以,该模式要尽量避免使用

function Person(name,age){
    var p = new Object();
    p.name = name;
    p.age = age;
    p.sayName = function(){
        alert(this.name);
    }
    return p;
}
var p1 = new Person('mjj',28);
console.log(p1 instanceof Person);//false
console.log(p1.__proto__ === Person.prototype);//false
稳妥构造函数模式

​所谓稳妥对象指没有公共属性,而且方法也不引用this对象。稳妥对象最适合在一些安全环境中(这些环境会禁止使用this和new)或者在防止数据被其他应用程序改动时使用

​稳妥构造函数与寄生构造函数模式相似,但有两点不同,一是新创建对象的实例方法不引用this;二是不适用new操作符调用构造函数。

function Person(name,age){
    //创建要返回的对象
    var p = new Object();
    //可以在这里定义私有变量和函数
    //添加方法
    p.sayName = function (){
        console.log(name);
    }
    //返回对象
    return p;
}
//在稳妥模式创建的对象中,除了使用sayName()方法之外,没有其他方法访问name的值
var p1 = Person('mjj',28);
p1.sayName();//"mjj"

与寄生构造函数模式相似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此instanceof操作符对这种对象也没有什么意义

原型模式

​使用原型对象,可以让所有实例共享它的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中

function Person(){
    Person.prototype.name = "mjj";
    Person.prototype.age = 29;
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
}
var p1 = new Person();
p1.sayName();//"mjj"
var p2 = new Person();
p2.sayName();//"mjj"
alert(p1.sayName === p2.sayName);//true
更简单的原型模式

​为了减少不必要的输入,也为了从视觉上更好地封装原型的功能,用一个包含所有属性的方法的对象字面量来重写整个原型对象

​但是,经过对象字面量的改写后,constructor不再指向Person。因此此方法完全重写了默认的prototype对象,使得Person.prototype的自有属性constructor属性不存在,只有从原型链中找到Object.prototype中的constructor属性

function Person(){};
Person.prototype = {
    name:'mjj',
    age:28,
    sayName:function(){
        console.log(this.name);
    }
}
var p1 = new Person();
p1.sayName();//"mjj"
console.log(p1.constructor === Person);//false
console.log(p1.constructor === Object);//true

可以显示地设置原型对象的constructor属性

function Person(){};
Person.prototype = {
    constructor:Person,
    name:'mjj',
    age:28,
    sayName:function(){
        console.log(this.name);
    }
}
var p1 = new Person();
p1.sayName();//"mjj"
console.log(p1.constructor === Person);//true
console.log(p1.constructor === Object);//false

原型模式问题在于引用类型值属性会被所有的实例对象共享并修改,这也是很少有人单独使用原型模式的原因。

function Person(){};
Person.prototype = {
    constructor:Person,
    name:'mjj',
    age:28,
    friends:['alex','阿黄'],
    sayName:function(){
        console.log(this.name);
    }
}
var p1 = new Person();
var p2 = new Person();
p1.friends.push('阿黑');
alert(p1.friends);//['alex','阿黄','阿黑']
alert(p2.friends);//['alex','阿黄','阿黑']
alert(p1.friends === p2.friends);//true

组合模式

​组合使用构造函数模式和原型模式是创建自定义类型的最常见方式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,这种组合模式还支持向构造函数传递参数。实例对象都有自己的一份实例属性的副本,同时又共享对方法的引用,最大限度地节省了内存。该模式是目前使用最广泛、认同度最高的一种创建自定义对象的模式

 

function Person(name,age){
    this.name = name;
    this.age = age;
    this.friends = ['alex','阿黄'];
}
Person.prototype = {
    constructor:Person,
    sayName:function(){
        console.log(this.name);
    }
}
var p1 = new Person('mjj',28);
var p2 = new Person('jjm',30);
p1.friends.push('wusir');
alert(p1.friends);//['alex','阿黄','wusir']
alert(p2.friends);//['alex','阿黄']
alert(p1.friends === p2.friends);//false
alert(p1.sayName === p2.sayName);//true
动态原型模式

​动态原型模式将组合模式中分开使用的构造函数和原型对象都封装到构造函数中,然后通过检查方法是否被创建,来决定是否初始化原型对象

​使用这种方法将分开的构造函数和原型对象合并到了一起,使得代码更加整齐,也减少了全局控件的污染

function Person(name,age){
    //属性
    this.name = name;
    this.age = age;
    //方法
    if(typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            console.log(this.name);
        }
    }
}
var p1 = new Person('小马哥',28);
p1.sayName();//"小马哥"

总结

​从使用对象字面量形式创建一个对象开始说起,创建多个对象会造成代码冗余;使用工厂模式可以解决该问题,但存在对象识别的问题;接着介绍了构造函数模式,该模式解决了对象识别的问题,但存在关于方法的重复创建问题;接着介绍了原型模式,该模式的特点就在于共享,但引出了引用类型值属性会被所有的实例对象共享并修改的问题;最后,提出了构造函数和原型组合模式,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,这种组合模式还支持向构造函数传递参数,该模式是目前使用最广泛的一种模式。

原型链继承

JavaScript使用原型链作为实现继承的主要方法,实现的本质是重写原型对象,代之以一个新类型的实例。下面代码中,原来存在于Super的实例对象的属性和方法,现在也存在于Sub.prototype中了

function Super(){
    this.value = true;
}
Super.prototype.getValue = function(){
    return this.value
}
function Sub(){};
//Sub继承了Super
Sub.prototype = new Super();
Sub.prototype.constroctor = Sub;

var ins = new Sub();
console.log(ins.getValue());//true
以上代码定了两个类型:SuperSubSub继承了Super,而继承是通过创建Super实例,并将实例赋给Sub.prototype实现的。**实现的本质是重写对象,代之以一个新类型的属性。**换句话说,原来存在于Super的实例中的所有属性和方法,现在也存在与Sub.prototype中。如图所示。

上图可以看出,我们没有使用Sub默认提供的原型,而是给它换了一个新原型;这个新原型就是Super的实例。于是,新原型不仅具有作为一个Super的实例所拥有的属性和方法,而且它还指向了Super的原型。最终结果就是这样的:

ins=>Sub的原型=>Super的原型

getValue()方法仍然还在Sub.prototype中,但value属性则位于Sub.prototype中。这是因为value是一个实例属性,而getValue()则是一个原型方法。既然Sub.prototype现在是Super的实例,那么value位于该实例中。

此外,要注意ins.constructor现在指向的是 Super,这是因为原来 Sub.prototype 中的 constructor 被重写了的缘故。

​原型链最主要的问题私有原型属性会被实例共享,而这也正是为什么要在构造函数中,而不是原型对象中定义属性的原因。在通过原型来实现继承时,原型实例会变成另一个类型的实例。于是,原先的实例属性也就顺理成章的变成了现在的原型属性了。

function Super(){
    this.colors = ['red','green','blue'];
}
Super.prototype.getValue = function(){
    return this.colors
}
function Sub(){};
//Sub继承了Super
Sub.prototype = new Super();
var ins1 = new Super();
ins1.colors.push('black');
console.log(ins1.colors);//['red','green','blue','black'];
var ins2 = new Sub();
console.log(ins2.colors);//['red','green','blue','black'];
原型链的第二个问题,在创建子类型的实例时,不能向父类型的构造函数传递参数。实际上,应该说是没有办法在不影响所有都想实例的情况下,给父类型的构造函数传递参数。再加上包含引用类型值的原型属性会被所有实例共享的问题,在实践中很少会单独使用原型链继承

注意问题

​使用原型链继承方法要谨慎地定义方法,子类型有时候需要重写父类的某个方法,或者需要添加父类中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。

function Super() {
    this.colors = ['red', 'green', 'blue'];
}
Super.prototype.getValue = function() {
    return this.colors
}

function Sub() {
    this.colors = ['black'];
};
//Sub继承了Super
Sub.prototype = new Super();

//添加父类已存在的方法,会重写父类的方法
Sub.prototype.getValue = function() {
    return this.colors;
}
//添加父类不存在的方法
Sub.prototype.getSubValue = function(){
    return false;
}
var ins = new Sub();
//重写父类的方法之后得到的结果
console.log(ins.getValue()); //['black']
//在子类中新定义的方法得到的结果
console.log(ins.getSubValue());//false
//父类调用getValue()方法还是原来的值
console.log(new Super().getValue());//['red', 'green', 'blue']

借用构造函数继承

​借用构造函数的技术(有时候也叫做伪类继承或经典继承)。这种技术的基本思想相当简单,即在子类构造函数的内部调用父类构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()call()方法也可以在新创建的对象上执行构造函数。

function Super() {
    this.colors = ['red', 'green', 'blue'];
}
Super.prototype.getValue = function(){
    return this.colors;
}
function Sub(){
    //继承了Super
    Super.call(this);//相当于把构造函数Super中的this替换成了ins实例对象,这样在Super只有定义的私有属性会被继承下来,原型属性中定义的公共方法不会被继承下来
}
var ins = new Sub();
console.log(ins.colors);

传递参数

​相对于原型链来说,借用构造函数继承有一个很大的优势,即可以在子类构造函数中向父类构造函数传递参数

function B(name){
    this.name = name;
}
function A(){
    //继承了B,同时还传递了参数
    B.call(this,'MJJ');
    //实例属性
    this.age = 28;
}
var p = new A();
alert(p.name);//'MJJ'
alert(p.age);//28

借用构造函数的问题

​如果仅仅是借用构造函数,那么将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起。而且,在父类的原型中定义的方法,对子类而言是不可见的。所以这种方式使用较少

组合继承(重要)

组合继承,指的是将原型链和借用构造函数技术组合到一起,从而发挥两者之长的一种继承模式。其背后的思想是使用原型链实现对原型上的公共属性和方法的继承,而通过借用构造函数继承来实现对父类私有属性的继承。这样,即通过在父类原型上定义方法实现了函数复用,又能够保证每个实例都有父类的私有属性

function Super(name){
    this.name = name;
    this.colors = ['red','blue','green'];
}
Super.prototype.sayName = function(){
    alert(this.name);
}
function Sub(name,age){
    Super.call(this,name);
    this.age = age;
}
// 继承方法
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function(){
    alert(this.age);
}
var ins = new Sub('mjj',28);
ins.colors.push('black');
console.log(ins.colors);// ["red", "blue", "green", "black"]
ins.sayName();//'mjj'
ins.sayAge();//28

var ins2 = new Sub('alex',38);
console.log(ins2.colors);//["red", "blue", "green"]
ins2.sayName();//'alex'
ins2.sayAge();//38
在上个例子中,Sub构造函数定义了两个属性:nameageSuper的原型定义了一个sayName()方法。在Sub构造函数中调用Super构造函数时传入了name参数,紧接着又定义它自己的属性age。然后,将Super的实例赋值给Sub的原型,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让不同的Sub实例分别拥有自己的属性——包括colors属性,又可以使用相同的方法。

组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,称为JavaScript中最常用的继承模式。

组合继承的问题

无论在什么情况下,都会调用两次父类的构造函数:一次是在创建子类原型的时候,另一次是在子类构造函数内部。

寄生组合式继承

​组合继承是JavaScript最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数:一次是在创建子类原型的时候,另一次是在子类构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子 类型构造函数时重写这些属性。再来看一看下面组合继承的例子。

function Super(name){
    this.name = name;
    this.colors = ['red','blue','green'];
}
Super.prototype.sayName = function(){
    alert(this.name);
}
function Sub(name,age){
    Super.call(this,name);
    this.age = age;
}
// 继承方法
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function(){
    alert(this.age);
}
var ins = new Sub('mjj',28);
ins.colors.push('black');
console.log(ins.colors);// ["red", "blue", "green", "black"]
ins.sayName();//'mjj'
ins.sayAge();//28

var ins2 = new Sub('alex',38);
console.log(ins2.colors);//["red", "blue", "green"]
ins2.sayName();//'alex'
ins2.sayAge();//38

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背 后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型 原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型 的原型。寄生组合式继承的基本模式如下所示。

function Super(name){
    this.name = name;
    this.colors = ['red','blue','green'];
}
Super.prototype.sayName = function(){
    alert(this.name);
}
function Sub(name,age){
    //继承实例属性
    Super.call(this,name);
    this.age = age;
}
// 继承公有的方法
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function(){
    alert(this.age);
}
var ins = new Sub('mjj',28);
ins.colors.push('black');
console.log(ins.colors);// ["red", "blue", "green", "black"]
ins.sayName();//'mjj'
ins.sayAge();//28

var ins2 = new Sub('alex',38);
console.log(ins2.colors);//["red", "blue", "green"]
ins2.sayName();//'alex'
ins2.sayAge();//38

多重继承

JavaScript中不存在多重继承,那也就意味着一个对象不能同时继承多个对象,但是我们可以通过变通方法来实现。

 

转载于:https://juejin.cn/post/6891074464855883789

posted @   小叶_Jiang  阅读(66)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程
点击右上角即可分享
微信分享提示