js 面向对象(设计模式) 与 继承(原型与原型链)

目录

一、面向对象基本特征

二、理解对象

1、对象的属性

(1)、数据属性 和 访问器属性

(2)、Object.defineProperty() 方法

2、为对象定义多个属性—— Object.defineProperties() 方法

 3、获取对象属性的描述符—— getOwnPropertyDescriptor() 方法

三、创建对象

1、JS 常见的设计模式(★★★★★)

2、工厂模式

3、构造函数模式

(1)、用构造函数模式自定义对象

(2)、用 new 操作符来创建一个对象时,都经历了什么?

(3)、检测自定义对象类型

4、原型模式

(1)、constructor 属性、 [[Prototype]] 属性

(2)、确定原型与实例的关系

 (3)、在读取对象的某个属性时,都发生了什么?

(4)、in 操作符在原型中的运用

(5)、hasOwnProperty() 方法

(6)、Object.keys() 方法

(7)、简化原型语法

 (8)、原型的动态性

 (9)、原生对象的原型

(10)、原型对象的问题

5、通过构造函数和原型的混成模式来创建自定义类型(★)

6、动态原型模式

7、寄生构造函数模式

8、稳妥构造函数模式

四、继承

1、原型链的继承

(1)、JS 的原型 与 原型链(★★★★★)

(2)、原型链的继承实例

(3)、原型链的继承的注意事项

(4)、原型链存在的问题

2、原型的继承

(1)、原型的继承的实例

(2)、 Object.create() 方法规范了原型式继承

(3)、 原型式继承的问题

3、构造函数的继承

(1)、借用构造函数实现继承

 (2)、借用构造函数”实现继承存在的问题

4、组合继承

 (1)、组合继承的实现

(2)、组合继承的问题

5、寄生式继承

(1)、寄生式继承的实现

 (2)、寄生式继承的问题

6、寄生组合式继承(★)


一、面向对象基本特征

Javascript是一种基于对象的语言。但它又不是真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)—–es6以前是这样的。所以es5只有使用函数模拟的面向对象。

真正的面向对象编程有以下三个特征:

  • 封装:也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
  • 继承:通过继承创建的新类称为“子类”或“派生类”。继承的过程,就是从一般到特殊的过程。
  • 多态:对象的多功能,多方法,一个方法多种表现形式。

二、理解对象

  • 在 JavaScript中,对象是一个无序的集合,其属性可以包含基本值、对象和函数。
  • 在 JavaScript中,每一个对象都是给予一个引用类型创建的,这个引用类型可以是原生类型(戳此了解引用类型),也可以是开发者定义的类型。

对象的特征:

  • 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
  • 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
  • 对象具有行为:即对象的状态,可能因为它的行为产生变迁。

对象具有唯一标识的内存地址,所以具有唯一的标识。

JavaScript 中对象独有的特征是:对象具有高度的动态性,这是因为 JavaScript 赋予了使用者在运行时为对象添改状态和行为的能力。

实际上 JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者 Symbol 为 key,以数据属性特征值或者访问器属性特征值为 value。

1、对象的属性

JavaScript 的属性被设计成比别的语言更加复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。

javaScript 对象的三种属性:

  • 数据属性:拥有一个确定的值的属性。这也是最常见的属性。
  • 访问器属性:通过 getter 和 setter 进行读取和赋值的属性。
  • 内部属性:由 JavaScript 引擎内部使用的属性,ES6 之前必须用 Object.getPrototypeOf() 方法来读取和设置。比如:
    • 每个对象都有一个内部属性 [[Prototype]]。如果是函数对象,改变其 prototype 属性后,其下所有的实例也会发生改变。
    • 数组对象的 length 属性。
    • DOM 的 innerHTML,我们赋值时是一个字符串,再取出时,这字符串可能会与原来的不一样, 并且在原元素上生成了不一样的子节点。比如,某一数组,它的长度为10, 当我们设置它为11时,它就会增加一个undefined元素,再设置为9时,就会从后面删掉两个元素。

(1)、数据属性 和 访问器属性

configurableenumerablevaluewritablegetset
数据属性YesYesYesYesNoNo
访问器属性YesYesNoNoYesYes

数据属性包括:configurable、enumerable、value 和 writable。

访问器属性包括:configurable、enumerable、get 和 set。

  • configrable 当且仅当该属性的configrable为true时,value才能被修改、删除,默认false。
  • enumerable 当且仅当该属性的enumerable为true时,才允许该value出现在对象的枚举属性(比如:for...in 或 Object.keys())中,默认false。
  • value:该属性对应的值,默认为 undefined。
  • writable:当且仅当该属性的writable为true时,value才能被赋值运算符改变,否则报错。默认为 false。
  • get:在读取属性时调用的函数,默认值为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
  • set:在写入属性时调用的函数,默认值为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。

(2)、Object.defineProperty() 方法

  • 通过 Object.defineProperty() 方法可以创建一个变量。通过 Object.defineProperty() 方法也可以创建一个常量(当 writable: false 且 configurable: false 时,不可修改,不可重新定义或者删除),类似于 ES6 中的 const 声明的常量。
  • 通过 Object.defineProperty() 方法为定义象属性针对 数据属性 和 访问器属性 各有一套模式,不过,这两套模式不能混合使用(设置set或者get,就不能在设置value和wriable,否则会报错)。

①、用 Object.defineProperty() 方法 操作 数据属性(writable 和 value)
--> writable: true

let Person = {}
Object.defineProperty(Person, 'sex', {
    configurable: true,
    enumerable: true,
    value: '男',
    writable:true,
})
console.log('---给person对象写入一个属性', Person);
 
Person.sex = '女';
console.log('---修改后的person', Person);

 结果:

--> writable: false

let Person = {};
Object.defineProperty(Person, 'sex', {
    configurable: true,
    enumerable: true,
    value: '男',
    writable:false,
})
console.log('---给person对象写入一个属性', Person);

Person.sex = '女';
console.log('---修改后的person', Person);

结果:

②、用 Object.defineProperty() 方法 操作 访问器属性(getter 和 setter)

var book = {
    _year: 2027,
    edition: 1
};

Object.defineProperty(book, "year", {
    get: function(){
        return this._year;
    },
    set: function(newValue){
        if(newValue > 2027){
            this._year = newValue;
            this.edition += newValue - 2027;
        }
    }
});

book.year = 2029;
console.log(book.edition);              // 3

2、为对象定义多个属性—— Object.defineProperties() 方法

var book = {};

Object.defineProperties(book, {
    _year: {
        writable: true,
        value: 2027
    },
    edition: {
        writable: true,
        value: 1
    },
    year: {
        get: function(){
            return this._year;
        },
        set: function(newValue){
            if(newValue > 2027){
                this._year = newValue;
                this.edition += newValue - 2027;
            }
        }
    }
});

 3、获取对象属性的描述符—— getOwnPropertyDescriptor() 方法

对象属性的描述符也就是对象的内部属性:configurable、enumerable、value、writable、get 和 set。

var book = {};

Object.defineProperties(book, {
    _year: {
        value: 2027
    },
    edition: {
        value: 1
    },
    year: {
        get: function(){
            return this._year;
        },
        set: function(newValue){
            if(newValue > 2027){
                this._year = newValue;
                this.edition += newValue - 2027;
            }
        }
    }
});

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(descriptor.value);                    // 2027
console.log(descriptor.configurable);             // false
console.log(typeof descriptor.get);               // "undefined"

var descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value);                    // undefined
console.log(descriptor.configurable);             // false
console.log(typeof descriptor.get);               // "function"

三、创建对象

1、JS 常见的设计模式(★★★★★)

请戳这里:js 设计模式_weixin79893765432...的博客-CSDN博客_js设计模式 阮一峰

2、工厂模式

  • 虽然 Object构造函数 或 字面量 都可以创建一个对象,但是这些方式有一个明显的问题:使用同一个借口创建很多对象,会产生大量的的重复代码。为了解决这个问题,出现了 “工厂模式”。
  • 工厂模式抽象了创建具体对象的过程。考虑到 JavaScript 在 ES6 之前无法创建类,卡法人员就发明了一种函数,用函数来封装以特定接口创建对象的细节。
  • 工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
function createPerson(name, age, job){
    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("marry", 18, "doctor");
var person2 = createPerson("lily", 21, "teacher");
person1.sayName();            // marry
person2.sayName();            // lily

 上述代码,函数 createPerson() 能够根据接受的参数来构建一个三个属性一个方法的 person 对象。

3、构造函数模式

  • 构造函数始终都应该以一个大写字母开头。
  • 除了通过原生构造函数(Object、Array、Number 和 String)来创建特定的对象类型外,我们还可以通过创建自定义的构造函数,从而定义 自定义对象类型 的属性和方法。
  • 当创建了自定义的构造函数后,其原型只会取得 constructor 属性,其他方法都是从原生 Object类型 继承而来。
  • 构造函数模式 既解决了创建多个相似对象的问题,又解决了对象识别的问题(即怎样知道一个对象的类型)。

(1)、用构造函数模式自定义对象

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("marry", 18, "doctor");
var person2 = new Person("lily", 21, "teacher");
person1.sayName();            // marry
person2.sayName();            // lily

 与工厂模式的案例相比,上述代码中,Person() 函数取代了 createPerson() 函数。他们的不同之处在于:

  • Person() 函数没有显式的创建对象;
  • Person() 函数直接将属性和方法赋值给了 this 对象;
  • Person() 函数没有 return 语句。

(2)、用 new 操作符来创建一个对象时,都经历了什么?

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

更细致的讲,new 运算接受一个构造器和一组调用参数,实际上做了几件事:

  • 以构造器的 prototype 属性(注意与私有字段[[prototype]]的区分)为原型,创建新对象;
  • 将 this 和调用参数传给构造器,执行;
  • 如果构造器返回的是对象,则返回,否则返回第一步创建的对象。

(3)、检测自定义对象类型

  • 通过对象的 constructor 属性来检测自定义对象的类型。不过,一般检测对象类型都用 instanceof 操作符,instanceof 操作符可以检测所有的对象类型,所以它也可以检测自定义对象类型。
  • 通过构造函数创建的自定义对象,既是Object 的实例,同时又是 “自定义对象类型” 的实例。
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("marry", 18, "doctor");
var person2 = new Person("lily", 21, "teacher");

console.log(person1.constructor == Person);        // true
console.log(person2.constructor == Person);        // true

console.log(person1 instanceof Person);            // true
console.log(person2 instanceof Person);            // true

console.log(person1 instanceof Object);            // true
console.log(person2 instanceof Object);            // true

上述代码中,为什么 person1 对象 和 person2 对象 既是自定义的 Person 构造函数模式的实例又是 Object 的实例呢?

因为所有对象均继承自 Object。(关于“继承”,请继续往下看。)

4、原型模式

我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,这个原型对象储存着原型上的属性和方法。使用原型对象可以让所有的对象实例共享它包含的属性和方法,换句换说,不必再构造函数中定义对象的实例信息,而是可以将这些信息直接添加到原型对象中。

原型系统可以说相当简单,我可以用两条概括:

  • 如果所有对象都有私有字段[[prototype]],就是对象的原型;
  • 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。

从 ES6 以来,JavaScript 提供了一系列内置函数,以便更为直接地访问操纵原型。三个方法分别为:

  • Object.create 根据指定的原型创建新对象,原型可以是 null;
  • Object.getPrototypeOf 获得一个对象的原型;
  • Object.setPrototypeOf 设置一个对象的原型。利用这三个方法,我们可以完全抛开类的思维,利用原型来实现抽象和复用。
function Person(){
    Person.prototype.name = "marry";
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
}

var person1 = new Person();
var person2 = new Person();
person1.sayName();                                        // marry
person2.sayName();                                        // marry
console.log(person1.sayName === person2.sayName);         // true

上述代码,我将 sayName() 方法和所有属性直接添加到了 Person 的 prototype 属性中,仍然可以通过构造函数来创建对象,但与构造函数不同的是,新对象的这些属性和方法是有所有实例共享的。

(1)、constructor 属性、 [[Prototype]] 属性

  • constructor 属性:在默认情况下,所有原型对象都会自动获得一个 constructor 属性,这个属性是一个指向 prototype 属性所在函数的指针。
  •  [[Prototype]] 属性:当创建了自定义的构造函数后,其原型只会取得 constructor 属性,其他方法都是从原生 Object类型 继承而来。当调用构造函数创建一个新实例后,该实例内部将包含一个指针(内部属性 [[Prototype]]),指向构造函数的原型对象。借助浏览器可以访问到——浏览器在每个对象上都支持一个属性:__proto__ 属性。通过__proto__ 属性可以访问到 [[Prototype]]。

以 Person 构造函数 和 Person.prototype 创建的实例的代码为例:

function Person(){
    Person.prototype.name = "marry";
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
}

var person1 = new Person();
var person2 = new Person();
person1.sayName();                                                     // marry
person2.sayName();                                                     // marry
console.log(Person.prototype.constructor === Person);                  // true
console.log(Object.getPrototypeOf(person1) === Person.prototype);      // true 
console.log(Object.getPrototypeOf(person2).name);                      // "marry"

上水代码中,Person 构造函数、Person 的原型属性以及 Person 现有的两个实例之间的关系如下图: 

 

 由上图可知,Person 的每个实例都包含一个内部属性 [[prototype]],该属性指向了 Person.prototype。虽然这两个实例不包含属性和方法,但是由于内部属性 [[prototype]] 属性指向了 Person.prototype,所以我们就可以调用 Person1.sayName() 了。

(2)、确定原型与实例的关系

有 3中方法可以确定原型与实例的关系:

  • instanceof 操作符;
  • Object.prototype.isPrototypeOf() 方法;
  • Object.getPrototypeOf() 方法。

其中 Object.getPrototypeOf() 方法会返回该构造函数的原型的值。

function Person(){
    Person.prototype.name = "marry";
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
}

var person1 = new Person();
var person2 = new Person();
console.log(Object.getPrototypeOf(person1) === Person.prototype);      // true 
console.log(Object.getPrototypeOf(person2).name);                      // "marry"
console.log(Object.prototype.isPrototypeOf(person1));                  // true
console.log(Object.prototype.isPrototypeOf(person2));                  // true
console.log(person1 instanceof Object);                                // true
console.log(person1 instanceof Person);                                // true

 (3)、在读取对象的某个属性时,都发生了什么?

每当代码读取对象的某个属性时,都会先后执行两次搜索,搜索首先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,就返回该属性的值,这就是第一次搜索;如果没找到,就原型对象中继续查找具有给定名字的属性,如果在原型对象中找到了这个属性,就返回该属性的值,还没找到的话,就报错未定义,这就是第二次搜索。

例如,在实例中修改原型中的值

function Person(){
}
Person.prototype.name = "marry";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();
person1.name = "lily";
console.log(person1.name);               // lily
console.log(person2.name);               // marry

delete person1.name;
console.log(person1.name);               // marry

由上述代码可知,在实例中添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。其原理正是“每当代码读取对象的某个属性时,都会先后在实例中搜索,找不到时再在原型上搜索”。另外,使用 delete 操作符可以完全删除实例属性,从而恢复了对原型中属性的链接,让我们能够重新访问原型中的属性。

(4)、in 操作符在原型中的运用

in 操作符有两种用法:

  • for-in:遍历可枚举属性;
  • 单独使用:检测对象能否访问到属性,只要对象能够访问到属性就返回 true。
function Person(){
}

Person.prototype.name = "marry";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();
person2.name = "lily";

console.log("name" in person1);            // true
console.log("name" in person2);            // true
console.log("age" in person1);             // false

(5)、hasOwnProperty() 方法

hasOwnProperty() 方法用于检测一个属性是否存在于实例中,如果存在就返回 true。

function Person(){
}

Person.prototype.name = "marry";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();
person2.name = "lily";

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

老版本的 IE 的 DOM Element 是没有 hasOwnProperty 方法的。另外, window 对象也没有 hasOwnProperty 方法的。怎么办?可以考虑用 Object 对象的 hasOwnProperty 试试。

语法:Object.prototype.hasOwnProperty.call(对象名, 属性名);

  • call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数(这里的函数指的是 hasOwnProperty())。
function MyObj(name, attr) {
    this.name = name;
    this.sayHi = function () {
        return 'hi boy';
    }
}
var obj = new MyObj();

var arr = [];
for (var item in obj) {
    if(Object.hasOwnProperty.call(obj, item)){
        arr.push(item);
    }
}
console.log(arr);                 //  ["name", "sayHi"]

与 Object.keys() 方法的对比详见: js 获取对象内属性的个数以及获取对象的属性和方法(Object.keys()、Object.getOwnPropertyNames()、for...in...对比)_weixin79893765432...的博客-CSDN博客

(6)、Object.keys() 方法

Object.keys() 方法用于获取对象上所有可枚举的实例属性,和 for-in 语句的作用一样。Object.keys() 方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

function Person(){
}

Person.prototype.name = "marry";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var keys = Object.keys(Person.prototype);
console.log(keys);                           // ["name", "sayName"]

var p1 = new Person();
p1.name = "lily";
p1.age = 22;
var p1keys = Object.keys(p1);
console.log(p1keys);                         // ["name", "age"]

上述代码中,Object.keys() 方法基于原型使用时,就返回包含所有原型中属性的数组;基于实例使用时,就返回包含所有实例中属性的数组。

(7)、简化原型语法

用一个对象字面量来表示原型对象,这个对象字面量包含所有原型的属性和方法。

例如:

function Person(){
}

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

 (8)、原型的动态性

根据需求,随时重写原型,然后创建实例并调用原型,新的原型生效。若在重写之前创建实例,调用重写后的原型属性,会报错。

function Person(){
}

var friend = new Person;
Person.prototype = {
    name: "marry",
    sayName: function(){
        console.log(this.name);
    }
};

var friend2 = new Person();

friend2.sayName();                  // marry
friend.sayName();                   // error

重写原型对象,会切断现有原型与任何之前已经存在的对象实例的联系。 

 (9)、原生对象的原型

所有原生引用类型(Object、String、Number 等)都在其构造函数的原型上定义了方法,通过访问原生对象的原型,不仅可以区的所有默认方法的引用,而且还可以定义新方法, 可以像修改自定义对象的原型一样修改原生对象的原型。

例如:

String.prototype.startsWith = function(text){
    return this.indexOf(text) == 0;
};

var str = "hi boy";
console.log(str.startsWith('hi'));        // true

(10)、原型对象的问题

原型中所有属性是被多个实例共享的,对于包含引用类型值的属性来说,通过实例修改引用类型的值时,违背了“实例都是有自己的属性的”这一原则,不但改变了当前实例,还擅自改变了原型对象的该属性的值

function Person(){
}
Person.prototype = {
    name: 'marry',
    age: 18,
    friends:['lily', 'jock'],
    sayName: function(){
        console.log(this.name);
    }
};

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

person1.age = '21';
person1.friends.push('mack');

console.log(person1.age);            // 21
console.log(person2.age);            // 18

console.log(person1.friends);        // ["lily", "jock", "mack"]
console.log(person2.friends);        // ["lily", "jock", "mack"]

上述代码中,我们基于自定义类型 Person 构造函数创建了两个实例,接着,我们修改了 person1.age(基本类型值)和 person1.friends(引用类型值)。最后分别打印实例 person1 和 实例 person2 中的 age 和 friends 的值,发现只有基本类型值遵循了“实例都是有自己的属性的”,person1.age 的修改并没有影响到 Person.prototype 对象中的age的值,所以,person2.age 依然是18。但是通过实例修改引用类型的值时,违背了“实例都是有自己的属性的”这一原则,不但改变了当前实例,还擅自改变了原型对象的该属性的值

5、通过构造函数和原型的混成模式来创建自定义类型(★)

  • 创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式。
  • 构造函数模式用于定义实例的属性,原型模式用于定义方法和共享的属性。于是,每个实例都会有一份实例属性的副本,同时又共享着对方法的引用,最大限度的节省了内存。
  • 这种构造函数和原型的混成模式,支持向构造函数传递参数。

例如:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Marry", "Lily"];
}
Person.prototype = {
    constructor: Person,
    sayName: function(){
        console.log(this.name);
    }
}
var person1 = new Person("Jack", 18, "Doctor");
var person2 = new Person("Baer", 26, "Teacher");

person1.friends.push("Mike");

console.log(person1.friends);                   // ["Marry", "Lily", "Mike"]
console.log(person2.friends);                   // ["Marry", "Lily"]

person1.sayName();                              // Jack
person2.sayName();                              // Baer

6、动态原型模式

  • 动态原型模式把所有的信息都封装在了构造函数中,通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的有点。
  • 必要情况下才启动原型:通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
  • 如何检查某个应该存在的方法是否有效呢?
    • 用 typeof 操作符 和 instanceof 操作符来检测。

比如: 

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    if(typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            console.log(this.name);
            
        };
    }            
}

var friend = new Person("marry", 21, "Doctor");
friend.sayName();                  // marry

friend.sayName = function(){
    console.log('111111');
}
friend.sayName();                  // 111111

7、寄生构造函数模式

  • 通常在前述的几种模式均不使用的情况下,可以使用寄生构造函数模式。
  • 寄生构造函数模式与工厂模式很是相似,唯一不同的地方是:创建实例时,使用 new 操作符。
  • 寄生构造函数模式,返回的对象与构造函数或者与构造函数的原型属性没有任何关系。这样就不能依赖 typeof 或者 instanceof 来确定对象的类型了。由于存在这个问题,所以不推荐使用。
function Person(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job
    o.sayName = function(){
        console.log(this.name);
    };
    return o;
}

var person1 = new Person("marry", 18, "doctor");
person1.sayName();            // marry

构造函数在不返回值的情况下,默认返回实例对象。所以,这里既然 return 了指定的对象,就不返回实例对象了。

8、稳妥构造函数模式

  • 稳妥构造函数模式创建的对象,没有公共属性和公共方法,全部私有化。
  • 稳妥构造函数模式与寄生构造函数模式类似,但有两点不同:
    • 新创建的对象的实例方法不能用 this;
    • 不使用 new 操作符调用构造函数。
  • 稳妥构造函数模式,返回的对象与构造函数或者与构造函数的原型属性没有任何关系。这样就不能依赖 typeof 或者 instanceof 来确定对象的类型了。由于存在这个问题,所以不推荐使用。
function Person(name, age, job){
    var o = new Object();
    o.sayName = function(){
        console.log(name);
    };
    return o;
}

var friend = Person("marry", 21, "Doctor");
friend.sayName();                 // marry
console.log(friend.name);         // undefined

可见,在稳妥构造函数模式下,除了使用 sayName 方法外,没有其他办法访问 name 的值。

四、继承

  • 许多面向对象语言都支持两种继承方式:接口继承 和 实现继承。
  • 接口继承只继承方法名,而实现继承则继承实际的方法。
  • ECMAScript 只支持实现继承,而且其实现继承主要依赖于原型链。

1、原型链的继承

(1)、JS 的原型 与 原型链(★★★★★)

请戳这里:js 原型与原型链_weixin79893765432...的博客-CSDN博客

(2)、原型链的继承实例

// 超类型
function SuPerson(){
    this.property = true;
}
SuPerson.prototype.getSuperValue = function(){
    console.log(this.property);
}

// 子类型
function Person(){
}

// 继承
Person.prototype = new SuPerson();

var instance = new Person();
instance.getSuperValue();                // true

(3)、原型链的继承的注意事项

  • 子类型有时需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但是不管怎样,给原型添加的代码必须放在替换原型的语句之后
  • 在通过原型链实现继承时,不能使用对象字面量创建原型方法,因为用字面量创建创建原型会重写原型链,导致之前的属性无效而报错。上文中有过详解。

(4)、原型链存在的问题

  • 原型链虽然可以用来实现继承,但是他也存在一些问题,最主要的问题来自包含引用类型值的原型。上文中详解过:包含引用类型值的原型属性会被所有实例共享。这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。
  • 在创建子类型的实例时,不能向超类型的构造函数中传递参数

2、原型的继承

(1)、原型的继承的实例

原型式继承,是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

  • 原型式继承必须有一个对象可以作为另一个对象的基础。

创建一个函数,比如 object() 函数(注意不是自定义类型哦,自定义类型首字符必须大写的),在 object() 函数内部,先创建一个临时性构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的新实例。本质上,object() 函数对传入的对象做了一次浅拷贝。

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}

// 参照对象
var person = {
    name: "marry",
    friends: ["bob", "lily"]
};

var anotherPerson = object(person);
anotherPerson.name = "baer";
anotherPerson.friends.push("jack");

console.log(person.name);                         // marry
console.log(anotherPerson.name);                  // baer
console.log(person.friends);                      // ["bob", "lily", "jack"]
console.log(anotherPerson.friends);               // ["bob", "lily", "jack"]

(2)、 Object.create() 方法规范了原型式继承

Object.create() 方法接收两个参数:一个作为新对象原型的对象 和 (可选的)一个为新对象定义额外属性的对象。

①、只传入一个参数时

Object.create() 方法,在只传入一个参数的情况下,与上文中的 object() 函数的原理相同。

// 参照对象
var person = {
    name: "marry",
    friends: ["bob", "lily"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "baer";
anotherPerson.friends.push("jack");

console.log(person.name);                         // marry
console.log(anotherPerson.name);                  // baer
console.log(person.friends);                      // ["bob", "lily", "jack"]
console.log(anotherPerson.friends);               // ["bob", "lily", "jack"]

②、传入两个参数时

 Object.create() 方法,在传入两个参数的情况下,第二个参数指定的任何对象的属性,都会覆盖参照对象的同名属性,若不同名就在新对象上新增该属性。

// 参照对象
var person = {
    name: "marry",
    friends: ["bob", "lily"]
};

var anotherPerson = Object.create(person, {
    name: {
        value: "tony"
    },
    age: {
        value: 18
    },
    sayAge: {
        value: function(){
            console.log(this.age);
        }
    }
});
anotherPerson.friends.push("jack");

console.log(person.name);                         // marry
console.log(anotherPerson.name);                  // baer
console.log(person.friends);                      // ["bob", "lily", "jack"]
console.log(anotherPerson.friends);               // ["bob", "lily", "jack"]
console.log(anotherPerson.age);                   // 18
anotherPerson.sayAge();                           // 18

(3)、 原型式继承的问题

显而易见,原型式继承也存在 “包含引用类型值的属性始终都会共享相应的值” 的问题。

3、构造函数的继承

“借用构造函数” 技术的基本思想是:通过使用 apply() 和 call() 方法,在子类型构造函数内部调用超类型构造函数。

  • 借用“构造函数”实现继承:解决了 原型中包含引用类型值所带来的问题。

(1)、借用构造函数实现继承

function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){
    // 继承
    SuperType.call(this);
}

var instance1 = new SubType();
var instance2 = new SubType();
instance1.colors.push("black");

console.log(instance1.colors);                     // ["red", "blue", "green", "black"]
console.log(instance2.colors);                     // ["red", "blue", "green"]

“借用构造函数” 技术可以通过 子类型构造函数 向 超类型构造函数 传递参数:

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

function SubType(){
    SuperType.call(this, "marry");
    this.age = 18;
}

var instance = new SubType();
console.log(instance.name);                // "marry"
console.log(instance.age);                 // 18

 (2)、借用构造函数”实现继承存在的问题

单独使用 “借用构造函数” 存在两个问题:

  • “借用构造函数” 的方法都在构造函数中定义,因此函数复用就无从谈起了。
  • “借用构造函数” 在超类型的原型中定义的方法,对子类型而言是不可见的,结果所有类型都只能使用构造函数模式。

所以,几乎不单独使用 “借用构造函数”。

4、组合继承

组合继承是将 “借用构造函数” 和 原型链 组合到一块。避免了 “借用构造函数” 和 原型链 的缺陷,融合了他们的优点,是 JavaScript 中最常用的继承模式。

  • 解决了包含引用类型值的原型属性会被所有实例共享的问题
  • 解决了通过子类型构造函数不能向超类型构造函数传递参数的问题
  • 解决了“借用构造函数”不能复用的问题
  • 解决了“借用构造函数”原型中定义的方法对子类型不可见的问题

 (1)、组合继承的实现

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
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 instance1 = new SubType("marry", 18);
instance1.colors.push("black");
console.log(instance1.colors);                         // ["red", "blue", "green", "black"]
instance1.sayName();                                   // marry
instance1.sayAge();                                    // 18

var instance2 = new SubType();
console.log(instance2.colors);                        // ["red", "blue", "green"]

(2)、组合继承的问题

组合继承都会调用 2 次超类型构造函数:一次是在创建子类型原型的时候;另一次是在子类型构造函数内部。 那么问题来了:在调用超类型构造函数创建子类型原型时,子类型已经继承了超类型对象的全部属性,但我们却在调用子类型构造函数时重写了这些属性,多此一举。

5、寄生式继承

寄生式继承是基于原型式继承的扩展,与寄生构造函数 和 工厂模式类似,即创建一个仅用于封装继承过程的函数。

(1)、寄生式继承的实现

比如,创建一个仅用于封装继承过程的函数 createAnother(),通过调用 object() 函数生成一个原对象(original)副本的实例对象(clone),然后为其添加它的私有属性和方法,最后作为返回这个加工后的实例对象(clone)。

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}

function createAnother(original){
    var clone = object(original);
    clone.sayHi = function(){
        console.log("Hi!");
    };
    return clone;
}

// 参照对象
var person = {
    name: "marry",
    friends: ["bob", "lily"]
};

var anotherPerson = createAnother(person);
anotherPerson.name = "baer";
anotherPerson.friends.push("jack");

console.log(person.name);                         // marry
console.log(anotherPerson.name);                  // baer
console.log(person.friends);                      // ["bob", "lily", "jack"]
console.log(anotherPerson.friends);               // ["bob", "lily", "jack"]
anotherPerson.sayHi();                            // Hi!

 (2)、寄生式继承的问题

寄生式继承的问题有 2 个:

  • 存在 “包含引用类型值的属性始终都会共享相应的值” 的问题。
  • 不能做到函数复用而降低了效率,这一点与构造函数模式类似。

6、寄生组合式继承(★)

  • 寄生组合式继承,通过 借用构造函数 来继承属性,通过 原型链的混合形式 来集成方法。
  • 寄生组合式继承,本质上,就是使用寄生式继承来继承超类型,然后再将结果指定给子类型的原型。
  • 寄生组合式继承只需调用超类型一次,解决了组合继承必须调用二次超类型的问题。
function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    console.log(this.name);
};

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

inheritPrototype(SubType, SuperType); // 继承方法

SubType.prototype.sayAge = function(){
    console.log(this.age);
};

var instance = new SubType("marry", 21);
instance.sayName();                   // marry
instance.sayAge();                    // 21

在ES6之前,寄生组合式继承,是最理想的继承范式。

【推荐阅读】

函数式编程与面向对象编程icon-default.png?t=M276https://blog.csdn.net/mChales_Liu/article/details/106530145js 原型与原型链icon-default.png?t=M276https://blog.csdn.net/mChales_Liu/article/details/109686177

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值