谈谈javascript的继承

  javascript是一门基于原型链的语言,而继承主要是通过原型链来实现的,所谓的原型链就是一条继承链,即多个实例对象共享同一个原型的属性和方法。

一、原型中的术语

  在原型中,有很多小伙伴对原型方面的理解存在很大的误区,笔者觉得是对原型中的术语理解不彻底或者混淆了概念。想要了解js的原型继承,需要对面向对象知识中的对象、原型、原型链、构造函数等基础知识掌握透彻,因此给小伙伴们一一介绍下原型中各位“大哥”:


在这里插入图片描述

1、对象

  其实所谓的对象就是一个包含相关数据和方法的集合(通常由一些变量和函数组成,我们称之为对象里面的属性和方法),在javascript中,所有的事物都是对象:包括基本的数据类型字符串、数值、数组、函数等,而且是允许自定义对象的。
  听到这,单身狗们是不是瞬间兴奋了,在哪?还可以自定义的,美滋滋,神器啊,要多new几个
在这里插入图片描述
  调皮一下很舒服,那有的小伙伴就好奇了,字符串也是对象?搞笑的吧,ok,我们来瞧瞧下面这张图哈:
在这里插入图片描述
  我们初始化了一个字符串stringA和stringB,对于stringB有length和split方法是因为new了一个String对象实例,这可以理解吧。但是stringA字符串却可以获取到length属性,更神奇的是还可以使用split方法,咋们并没有定义这些东西啊,怎么能说用就用呢,神奇吧?我们来打印个东西就清晰了:

console.log(stringA.__proto__ === String.prototype) // true

  其实stringA初始化时,是继承了String对象中的所有属性和方法,简单的说,是使用了javascript的内建对象,因此stringA能使用split方法并且能获取length,这其实就是原型中的一种基础继承。

2、constructor属性

  每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于构造此实例对象的构造函数

3、构造函数

  主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。其实也叫构造器,如:

     function Cat(name,color){
   this.name = name;
   this.color = color;
 }
 var cat1 = new Cat(‘猫哥’, '亮闪闪')
 cat1.name //猫哥

前端的构造函数特点:
a、构造函数的首字母必须大写,用来区分于普通函数
b、内部使用的this对象,来指向即将要生成的实例对象
c、使用New来生成实例对象
缺点:
  所有的实例对象都可以继承构造器函数中的属性和方法。但是,同一个对象实例之间,无法共享属性

4、原型对象(prototype)

  其他面向对象语言:面向对象的语言有一个标志,即拥有类(class)的概念,抽象实例对象的公共属性与方法,基于类可以创建任意多个实例对象,一般具有封装、继承、多态的特性!
  在javascript中,并不存在类的概念,所有实例对象需要共享的属性和方法,都放在一个对象中,那些不需要共享的属性和方法,就放在构造函数中,以此来模拟类,而这个对象指的就是prototype,即原型对象。
在这里插入图片描述
注:在前端中,prototype是函数才有的对象

5、__proto__

  用构造方法创建一个新的对象之后,这个对象中默认会有一个不可访问的属性 [[prototype]] , 这个属性就指向了构造方法的原型对象。

  __proto__ 属性是一个访问器属性(一个getter函数和一个setter函数), 暴露了通过它访问的对象的内部[[Prototype]] (一个对象或 null)。

  a、在ES5中,所有构造函数的__proto__都指向Function.prototype
  b、在ES6中,构造函数的__proto__指向它的父类构造函数

  注:绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.__proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj)。

6、实例

  实例是对象的具体表示,操作可以作用于实例,实例可以有状态地存储操作结果。实例被用来模拟现实世界中存在的、具体的或原型的东西。都是屁话,简单的说可以理解为实际的例子,构造函数通过new创建得到的对象。

二、原型链

  每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(constructor),而实例对象都包含一个指向原型对象的内部指针(__proto__)。如果让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针(__proto__),另一个原型也包含着一个指向另一个构造函数的指针(constructor)。假如另一个原型又是另一个类型的实例……这就构成了实例与原型的链条即原型链。

  简单的说,原型链的形成就是让一个类型的实例作为另一个类型的原型对象,原型对象中也有可能存在原型,层层剥离,从而形成原型链:这就是为什么一个对象中会拥有定义为其他对象中的属性和方法,也就是所谓的继承。
  ok,几位大哥已介绍完毕,来瞧瞧下面这波操作:

function Person() { 
} 
var person = new Person(); 

  虽然只是简单的几行代码,但是却包含了原型里的所有元素,先来看看内部的关系是怎么指向的。
在这里插入图片描述
  由相互关联的原型组成的链状结构就是原型链,而红色框框中的那一条关系就是原型链,原型链中的指向其实就是继承的本质。由上图也可得出:

console.log(person.__proto__===Person.prototype) //true 
console.log(Person.prototype.constructor===Person) //true 
console.log(Object.getPrototypeOf(person)===Person.prototype) //true

三、原型链存在的问题

  原型虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型。

1、包含引用类型值的原型,共享原型是存在问题的

function Animal () { 
}
Animal.prototype = {
	names = ["dog","cat"]; 
}
var animal1 = new Animal();
var animal2 = new Animal();
animal1.names.push("pig");
console.log(animal1.names); // "dog","cat","pig"
console.log(animal2.names); // "dog","cat","pig"

问题:通过引用实例改变了原型中本来中的值,同时也影响了其他实例。

解决:构造函数存的是私有属性和方法,而放在prototype下的是公用的属性和方法,所以引用类型值要定义在构造函数中而非原型中。

2、创建子类实例时,并不能给超类传递参数

什么是超类?在软件术语中,被继承的类一般称为“超类”,也有叫做父类。

function A (age) {
	this.Aage = age;
};
function B (age) {
	this.Bage = age;
};
//给B赋值的同时,想给A赋值,无法实现
B.prototype = new A();
var C = new B(12);
console.log(C.Aage);
console.log(C.Bage);

问题:在通过原型来继承时,B原型变成A类型的实例。于是原先的实例属性也就顺理成章的变成了现在原型属性了
但是我们看到A的实例C.name值为A,说明在B继承A的实例时是复制了A实例中的所有属性(包括prototype指针,形成原型链)并非引用
在这里插入图片描述
所以解决的方法只能使用call或者apply,手动调用A中的方法。
由于这两个问题的存在,实践中很少单独使用原型链。

四、其他继承方式

1、call和apply方法

使用call和apply方法进行继承:

function Animal() {
    this.animal = '动物'
}
Animal.prototype.getName = function() {
    console.log('我是dog')
}
function Cat() {
    Animal.apply(this, arguments)
}
var cat = new Cat()
cat.animal    // 动物
cat.getName()  // undefined

  这种方法可以继承父类构造函数的属性,但是无法继承prototype属性,即父类中共享的方法和属性,其实有点类似于借用下他爹的方法和属性,并不是实际拥有,内部的prototype原型对象并没有。

2、变量以及作用域链

  其实在javascript中,当变量和作用域创建时,会创建一个执行环境,而执行环境都是有一个作用域链的,在函数中,访问一个变量的时候,总是从作用域链的顶端开始查找,如果找到就得到结果,如果找到不到就一直查找,直到作用域链的末端。

function sum(num1, num2){
	var sum = num1 + num2;
	return sum;
}
var sum = sum(3, 4);

上面的函数具体查找方式如下:
在这里插入图片描述
  作用域链寻找函数对象的方式跟原型链有点类型,因此在这里也提一下。在javascript中,有几个比较特殊的对象也说下吧:
  顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
  顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计败笔之一

3、深拷贝(递归)

  还记得继承是什么吗?就是多个实例对象共享同一个原型的属性和方法。
  传统的继承一般是在创建时候,将构造函数中的属性和方法共享化,从而实现继承。而深拷贝在原型创建后,创建一个实例对象后,再将这个实例对象中的方法和属性拷贝到一个新的对象中,。

// 将obj2的成员拷贝到obj1中, 只拷贝实例成员
function deepCopy(obj1, obj2) {
    for (var key in obj2) {
        // 判断是否是obj2上的实例成员
        if (obj2.hasOwnProperty(key)) {
            // 判断是否是引用类型的成员变量
            if (typeof obj2[key] == 'object') {
                obj1[key] = Array.isArray(obj2[key]) ? [] : {};
                deepCopy(obj1[key], obj2[key]);
            } else {
                obj1[key] = obj2[key];
            }
        }
    }
}

本文由cjfpersonal小弟出版,希望大家踊跃吐槽哈

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值