JavaScript 中的继承

作者: 爱涂鸦呀 | 来源:发表于2018-04-16 22:05 被阅读138次

JS 继承机制的设计思想

Brendan Eich,

  • 借鉴 C++ 和 Java ,把 new 命令引入了 JS,用来从原型对象生成一个实例对象。
  • 他做了简化,在 JS 中,new 命令后面跟的不是类,而是构造函数。

new 构造函数缺点:

  • 构造函数,生成实例对象,无法共享属性和方法;
  • 每个实例对象,都有自己的属性和方法的副本,资源浪费。

引入 prototype 属性,

  • BE 为构造函数设置了一个 prototype 属性;
  • prototype 属性包含一个对象(prototype对象);
  • 所有实例对象需要共享的属性和方法,都放在 prototype对象里面;
  • 不需要共享的属性和方法,就放在构造函数里面。
  • 实例对象一旦创建,将自动引用prototype对象的属性和方法。

JS 继承机制的设计思想,

  • 由于所有的实例对象共享同一个prototype 对象,从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像“继承”了prototype对象一样。

JS 面向对象--封装

1. 生成实例对象的原始模式

var cat = {
    name:  '',
    color: ''
}

// ------------------------

var cat1 = {};
cat1.name = "Kitty";
cat1.color = "yellow";

var cat2 = {};
cat1.name = "Lucy";
cat1.color = "black";

缺点,

  1. 多生成几个实例,代码重复,写起来很麻烦;
  2. 实例与原型之间,没有任何办法,可以看出有什么联系。

2. 原始模式的改进

解决了代码重复,

function Cat(name, color) {
    return {
        name: name,
        color: color
    }
}

var cat1 = Cat("Kitty", "yellow");
var cat2 = Cat("Lucy", "black");

缺点,

  • cat1 和 cat2 之间仍然没有内在联系,不能反映出它们是同一个原型对象的实例。

3. 构造函数模式

为了解决从源性对象生成实例的问题,JS 提供了一个构造函数(Constructor)模式。

构造函数,

  • 实际就是一个普通函数;
  • 函数名大写以区分普通函数;
  • 内部使用this变量;
  • 对构造函数使用new运算符,生成实例,this变量会绑定在实例对象上。
function Cat(name, color) {
    this.name = name;
    this.color = color;
}

var cat1 = new Cat("Kitty", "yellow");
var cat2 = new Cat("Lucy", "black");

至此,

  • 代码重复解决了;
  • 实例对象都自动含有一个constructor属性,指向它们的构造函数。
  • JS 还提供了instanceof运算符,验证源性对象与实例之间的关系。
console.log(cat1.constructor == Cat);   // true
console.log(cat2.constructor == Cat);   // true

console.log(cat1 instanceof Cat);   // true
console.log(cat2 instanceof Cat);   // true

缺点,

  • 构造函数中的方法每次实例化对象,都必须为重复的内容,多占用一些内存。
  • 不能共享。

4. prototype 模式

JS 规定,

  • 每一个构造函数都有一个prototype属性,指向另一个对象;
  • 这个对象的所有属性和方法,都会被构造函数的实例继承。
function Cat(name, color) {
    this.name = name;
    this.color = color;
}
Cat.prototype.eat = function() {
    alert("eating mouse...");
}

var cat1 = new Cat("Kitty", "yellow");
var cat2 = new Cat("Lucy", "black");

cat1.eat(); // eating mouse...
cat2.eat(); // eating mouse...

至此,

  • 代码重复解决了;
  • 实例对象之间的联系明确了;
  • 属性和方法共享问题解决了。

5. prototype 模式的验证方法

  • isPrototypeOf(),用来判断,某个prototype对象和某个实例之间的关系;
  • hasOwnProperty(),用来判断,某个属性到底是本地属性,还是继承自prototype对象的属性;
  • in运算符,用来判断,某个实例是否含有某个属性,不管是不是本地属性;它还可以用来用来便利某个对象的所有属性。
// isPrototypeOf
console.log(Cat.prototype.isPrototypeOf(cat1)); // true
console.log(Cat.prototype.isPrototypeOf(cat2)); // true
// hasOwnProperty
console.log(cat1.hasOwnProperty("name"));   // true
console.log(cat2.hasOwnProperty("type"));   // false
// in
console.log("name" in cat1);    // true
console.log("type" in cat1);    // false
// 遍历属性
for(var prop in cat1) {
    console.log("cat1["+prop+"]="+cat1[prop]);
}

JS 面向对象--继承:构造函数的继承

对象之间的“继承”的 5 种方法,

// 两个构造函数

// 动物对象的构造函数
function Animal() {
    this.species = "动物";
}

// 猫对象的构造函数
function Cat(name, color) {
    this.name = name;
    this.color = color;
}

怎样才能使猫继动物呢???

1. 构造函数绑定

最简单的方法,使用callapply方法,将父对象的构造函数绑定到子对象上,即在子对象构造函数中加一行:

function Cat(name, color) {
    Animal.apply(this, arguments);  // 父对象的构造函数绑定到子对象上
    
    this.name = name;
    this.color = color;
}

var cat1 = new Cat("Kitty", "yellow");
alert(cat1.species);    // 动物

2. prototype 模式

最常见的方法,使用prototype属性。

如果的 prototype 对象,指向一个 Animal 实例,那么所有猫的实例,就能继承 Animal 了。

Cat.prototype = new Animal();       // 猫的 prototype 指向 Animal 的实例
Cat.prototype.constructor = Cat;    // 别忘了把构造函数的指向恢复成 Cat 哦

var cat1 = new Cat;
alert(cat1.species);    // 动物

编程时务必注意,

  • 如果替换了 prototype 对象;
  • 下一步必然是为了新的prototype对象加上contructor属性,并将这个属性只会原来的构造函数。
o.prototype = {};   // o 的 prototype对象 被新的对象{} 替换了
o.prototype.constructor = o; // 必须将新的 prototype 对象加上 constructor 属性,指回原来的构造函数

3. 直接继承 prototype

这是对第二种方法的改进,

  • 由于 Animal 对象中,不变的属性都可以直接写入 Animal.prototype
  • 所以可以让Cat()跳过Animal(),直接继承Animal.prototype
// 改写 Animal 对象
function Animal() {};
Animal.prototype.species = "动物";

Cat.prototype = Animal.prototype;   // 将 Cat的prototype对象直接指向Animal的prototype对象,就完成了继承
Cat.prototype.constructor = Cat;    // 这一步,实际上把 Animal.prototype对象的constructor属性也改掉了

var cat1 = new Cat("Kitty", "yellow");
alert(cat1.species);    // 动物

与第二种比,

  • 优点:效率比较高,不用执行和建立Animal实例了,省内存;
  • 缺点:Cat.prototypeAnimal.prototype现在指向了同一个对象,任何对Cat.prototype的修改,都会反映到Animal.prototype

4. 利用空对象作为中介

怎么解决上面的缺点?

可以用空对象作为中介。

var F = function() {};          // 空对象
F.prototype = Animal.prototype; // 空对象的原型指向动物的原型
Cat.prototype = new F();        // 原型指向新的实例对象,空对象几乎不占内存哦
Cat.prototype.constructor = Cat;// 此时,修改Cat的prototype对象,就不会影响到Animal的prototype对象

alert(Animal.prototype.constructor);    // Animal

将上面的方法,封装成一个函数,便于使用。

function extend(Child, Parent) {
    var F = function() {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    
    Child.uber = Parent.prototype;  // 为子对象设一个uber属性,这个属性直接指父对象的prototype属性,等于在子对象可以直接调用父对象的方法。只是为了实现继承的完备性,纯属备用性质。
}

// 使用的时候,方法如下
extend(Cat, Animal);
var cat1 = new Cat("Kitty", "yellow");
alert(cat1.species);    // 动物

5. 拷贝继承

上面是采用 prototype对象,实现继承。

换一种思路,纯粹采用”拷贝“方法实现继承,把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?

  • 首先,还是把 Animal的所有不变属性,都放到它的 prototype对象上。
function Animal() {};
Animal.prototype.species = "动物";
  • 然后,再写一个函数,实现属性拷贝的目的。
function extend2(Child, Parent) {
    var p = Parent.prototype;
    var c = Child.prototype;
    
    for(var i in p) {
        c[i] = p[i];    // 将父对象的属性一一拷贝给Child对象的prototype对象
    }
    
    c.uber = p;
    
}

// 使用的时候
extend2(Cat, Animal);
var cat1 = new Cat("Kitty", "yellow");
alert(cat1.species);    // 动物

JS 面向对象--继承: 非构造函数的继承

1. 什么是”非构造函数“的继承

var Chinese = {
    nation: '中国'
};

var Doctor = {
    career: '医生'
}

// 怎么样才能让 Doctor 去继承 Chinese 呢 ??
// 两个都是普通对象,不是构造函数,无法使用构造函数方法实现”继承“

2. object() 方法

json格式的发明人Douglas Crockford,提出了一个 object()函数,可以实现

function object(o) {
    function F() {}
    F.prototype = o;    // object()函数,其实只做一件事,就是把子对象的prototype属性指向父对象,从而使得子对象与父对象连在一起
    return new F();
}


// 使用的时候:
// 第一步,先在父对象的基础上,生成子对象;
var Doctor = object(Chinese);
// 然后,再加上子对象本身的属性:
Doctor.career = '医生';

// 这时,子对象已经继承了父对象的属性了
alert(Doctory.nation);  // 中国

3. 浅拷贝

// 浅拷贝

function extendCopy(p) {
    var c = {};
    
    for (var i in p) {
        c[i] = p[i];
    }
    
    c.uber = p;
    
    return c;
}

// 使用
var Doctor = extendCopy(Chinese);
Doctor.career = '医生';
alert(Doctor.nation);   // 中国

缺点,

  • 如果父对象的属性等于数组或另一个对象,那么,实际上子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。
  • extendCopy()只是拷贝基本类型的数据,我们把这种拷贝叫做浅拷贝。早期jQuery实现继承的方式。

4. 深拷贝

所谓 深拷贝,就是能够实现真正意义上的数组和对象的拷贝。只要递归调用 浅拷贝就行了。

function deepCopy(p, c) {
    var c = c || {};
    for (var i in p) {
        if(typeof p[i] === 'object') {
            c[i] = (p[i].constructor === Array) ? [] : {};
            deepCopy(p[i], c[i]);
        }else {
            c[i] = p[i];
        }
    }
    return c;
}

// 使用
var Doctor = deepCopy(Chinese); 

因为是值的深度拷贝,所以父对象不会收到影响。jQurey库目前使用的继承方法。


说明:以上皆为个人精简笔记
链接:阮一峰老师的博客

相关文章

  • 函数的原型对象

    什么是原型? 原型是Javascript中的继承的继承,JavaScript的继承就是基于原型的继承。 函数的原型...

  • 一文带你彻底理解 JavaScript 原型对象

    一、什么是原型 原型是Javascript中的继承的基础,JavaScript的继承就是基于原型的继承。 1.1 ...

  • Web前端经典面试试题及答案2

    javascript面向对象中继承实现? 面向对象的基本特征有:封闭、继承、多态。在JavaScript中实现继承...

  • 深入理解javascript中的继承机制 之 12种继承模式总结

    之前我们介绍了多种javascript中的继承方式,最后我们开始总结概括这些继承方式,先将javascript中的...

  • JavaScript - 继承和类

    JavaScript - 继承和类 在这一篇中,我要聊聊 JavaScript 中的继承和“类”。 首先跟你请教下...

  • Javascript中的继承

    很多语言中都有继承的概念,继承这种东西为什么会出现? 简而言之,继承之所以出现,就是为了减少重复无用功。好比爸爸有...

  • JavaScript 中的继承

    JS 继承机制的设计思想 Brendan Eich, 借鉴 C++ 和 Java ,把 new 命令引入了 JS,...

  • JavaScript 中的继承

    摘要:继承是面向对象思想中的重要概念,虽然严格来说 JavaScript 并属于面向对象类型的语言,但最终还是在E...

  • JavaScript 中的继承

    作者 魏楷聪 发布于 2015年01月20日 1) 对象冒充(支持多重继承) 2) call方法方式 call方法...

  • javascript中的继承

    继承是面向对象软件技术当中的一个概念,与多态、封装共为面向对象的三个基本特征。继承可以使得子类具有父类的属性和方法...

网友评论

    本文标题:JavaScript 中的继承

    本文链接:https://www.haomeiwen.com/subject/okdykftx.html