JS原形与原型链深入详解

本文实例讲述了JS原形与原型链。分享给大家供大家参考,具体如下:

前言

在JS中,我们经常会遇到原型。字面上的意思会让我们认为,是某个对象的原型,可用来继承。但是其实这样的理解是片面的,下面通过本文来了解原型与原型链的细节,再顺便谈谈继承的几种方式。

原型

在讲到原型之前,我们先来回顾一下JS中的对象。在JS中,万物皆对象,就像字符串、数值、布尔、数组等。ECMA-262把对象定义为:无序属性的集合,其属性可包含基本值、对象或函数。对象是拥有属性和方法的数据,为了描述这些事物,便有了原型的概念。

无论何时,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向该函数的原型对象。所有原型对象都会获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。

这段话摘自《JS高级程序设计》,很好理解,以创建实例的代码为例。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = function() {
    alert(this.name);
  };
}

const person1 = new Person("gali", 18);
const person2 = new Person("pig", 20);

上面例子中的person1跟person2都是构造函数Person()的实例,Person.prototype指向了Person函数的原型对象,而Person.prototype.constructor又指向Person。Person的每一个实例,都含有一个内部属性__proto__,指向Person.prototype,就像上图所示,因此就有下面的关系。

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

继承

JS是基于原型的语言,跟基于类的面向对象语言有所不同,JS中并没有类这个概念,有的是原型对象这个概念,原型对象作为一个模板,新对象可从原型对象中获得属性。那么JS具体是怎样继承的呢?

在讲到继承这个话题之前,我们先来理解原型链这个概念。

原型链

构造函数,原型和实例的关系已经很清楚了。每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例对象都包含一个指向与原型对象的指针。这样的关系非常好理解,但是如果我们想让原型对象等于另一个类型的实例对象呢?那么就会衍生出相同的关系,此时的原型对象就会含有一个指向另一个原型对象的指针,而另一个原型对象会含有一个指向另一个构造函数的指针。如果另一个原型对象又是另一个类型的实例对象呢?这样就构成了原型链。文字可能有点难理解,下面用代码举例。

function SuperType() {
  this.name = "张三";
}
SuperType.prototype.getSuperName = function() {
  return this.name;
};

function SubType() {
  this.subname = "李四";
}
SubType.prototype = new SuperType();
SubType.prototype.getSubName = function() {
  return this.subname;
};

const instance = new SubType();
console.log(instance.getSuperName()); // 张三

上述例子中,SubType的原型对象作为SuperType构造函数的实例对象,此时,SubType的原型对象就会有一个__proto__属性指向SuperType的原型对象,instance作为SubType的实例对象,必然能共享SubType的原型对象的属性,又因为SubType的原型对象又指向SuperType原型对象的属性,因此可得,instance继承了SuperType原型的所有属性。

我们都知道,所有函数的默认原型都是Object的实例,所以也能得出,SuperType的默认原型必然有一个__proto__指向Object.prototype。

图中由__proto__属性组成的链子,就是原型链,原型链的终点就是null

上图可很清晰的看出原型链的结构,这不禁让我想到JS的一个运算符instanceof,instanceof可用来判断一个实例对象是否属于一个构造函数。

A instanceof B; // true

实现原理其实就是在A的原型链上寻找是否有原型等于B.prototype,如果一直找到A原型链的顶端null,仍然找不到原型等于B.prototype,那么就可返回false。下面手写一个instanceof,这个也是很多大厂常用的手写面试题。

function Instance(left, right) {
  left = left.__proto__;
  right = right.prototype;
  while (true) {
    if (left === null) return false;
    if (left === right) return true;
    // 继续在left的原型链向上找
    left = left.__propo__;
  }
}

原型链继承

上面例子中,instance继承了SuperType原型的属性,其继承的原理其实就是通过原型链实现的。原型链很强大,可用来实现继承。可是单纯的原型链继承也是有问题存在的。

  • 实例属性变成原型属性,影响其他实例
  • 创建子类型的实例时,不能向超类型的构造函数传递参数
function SuperType() {
  this.colorArr = ["red", "blue", "green"];
}
function SubType() {}
SubType.prototype = new SuperType();

const instance1 = new SubType();
instance1.colorArr.push("black");
console.log(instance1.colorArr); // ["red", "blue", "green", "black"]

const instance2 = new SubType();
console.log(instance2.colorArr); // ["red", "blue", "green", "black"]

当SubType的原型作为SuperType的实例时,此时SubType的实例对象通过原型链继承到colorArr属性,当修改了其中一个实例对象从原型链中继承到的原型属性时,便会影响到其他实例。对instance1.colorArr的修改,在instance2.colorArr便能体现出来。

组合继承

组合继承指的是组合原型链和构造函数的技术,通过原型链实现对原型属性和方法的继承,而通过借用构造函数实现对实例属性的继承。

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);
};

const instance1 = new SubType("gali", 18);
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
instance1.sayName(); // gali
instance1.sayAge(); // 18

const instance2 = new SubType("pig", 20);
console.log(instance2.colors); // ["red", "blue", "green"]
instance2.sayName(); // pig
instance2.sayAge(); // 20

上述例子中,借用构造函数继承实例属性,通过原型继承原型属性与方法。这样就可让不同的实例分别拥有自己的属性,又可共享相同的方法。而不会像原型继承那样,对实例属性的修改影响到了其他实例。组合继承是JS最常用的继承方式。

寄生组合式继承

虽然说组合继承是最常用的继承方式,但是有没有发现,就上面的例子中,组合继承中调用了2次SuperType函数。回忆一下,在第一次调用SubType时。

SubType.prototype = new SuperType();

这里调用完之后,SubType.prototype会从SuperType继承到2个属性:name和colors。这2个属性存在SubType的原型中。而在第二次调用时,就是在创造实例对象时,调用了SubType构造函数,也就会再调用一次SuperType构造函数。

SuperType.call(this, name);

第二次调用之后,便会在新的实例对象上创建了实例属性:name和colors。也就是说,这个时候,实例对象跟原型对象拥有2个同名属性。这样实在是浪费,效率又低。

为了解决这个问题,引入了寄生组合继承方式。重点就在于,不需要为了定义SubType的原型而去调用SuperType构造函数,此时只需要SuperType原型的一个副本,并将其赋值给SubType的原型即可。

function InheritPrototype(subType, superType) {
  // 创建超类型原型的一个副本
  const prototype = Object(superType.prototype);
  // 添加constructor属性,因为重写原型会失去constructor属性
  prototype.constructor = subType;
  subType.prototype = prototype;
}

将组合继承中的:

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

替换成:

InheritPrototype(SubType, SuperType);

寄生组合继承的优点在于,只需要调用一次SuperType构造函数。避免了在SubType的原型上创建多余的不必要的属性。

总结

温故而知新,再次看回《JS高级程序设计》这本书的原型与原型链部分,发现很多以前忽略掉的知识点。而这次回看这个知识点,并输出了一篇文章,对我来说受益匪浅。写文章往往不是为了写出怎样的文章,其实中间学习的过程才是最享受的。

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。

更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

(0)

相关推荐

  • 深入理解javascript原型链和继承

    在上一篇文章中,介绍了原型的概念,了解到在javascript中构造函数.原型对象.实例三个好基友之间的关系:每一个构造函数都有一个"守护神"--原型对象,原型对象心里面也存着一个构造函数的"位置",两情相悦,而实例呢却又"暗恋"着原型对象,她也在心里留存了一个原型对象的位置. javascript本身不是面向对象的语言,而是基于对象的语言,对于习惯了其他OO语言的人来说,起初有些不适应,因为在这里没有"类"的概念,或者说&q

  • javascript 原型与原型链的理解及应用实例分析

    本文实例讲述了javascript 原型与原型链的理解及应用.分享给大家供大家参考,具体如下: javascript中一切皆对象,但是由于没有Class类的概念,所以就无法很好的表达对象与对象之间的关系了. 比如对象A与对象B之间,它们两个是相对独立的个体,互不干扰,对象A修改自身的属性不会影响到对象B. 虽然这很好,但是有一个问题,如果对象A与对象B都有一个方法 run() ,并且代码也一样,那对象A与对象B各自都独立拥有一份 run() 方法的完整代码,这是需要资源去保存的. 一旦我们程序中

  • JS继承--原型链继承和类式继承

    什么是继承啊?答:别人白给你的过程就叫继承. 为什么要用继承呢?答:捡现成的呗. 好吧,既然大家都想捡现成的,那就要学会怎么继承! 在了解之前,你需要先了解构造函数.对象.原型链等概念...... JS里常用的两种继承方式: 原型链继承(对象间的继承)类式继承(构造函数间的继承) 原型链继承: 复制代码 代码如下: //要继承的对象var parent={name : "baba" say : function(){ alert("I am baba");}} //

  • JavaScript原型及原型链终极详解

    JavaScript原型及原型链终极详解 一. 普通对象与函数对象 JavaScript 中,万物皆对象!但对象也是有区别的.分为普通对象和函数对象,Object,Function 是JS自带的函数对象.下面举例说明 function f1(){}; var f2 = function(){}; var f3 = new Function('str','console.log(str)'); var o3 = new f1(); var o1 = {}; var o2 =new Object()

  • JavaScript进阶(四)原型与原型链用法实例分析

    本文实例讲述了JavaScript原型与原型链用法.分享给大家供大家参考,具体如下: 一句话说明什么是原型:原型就是一个JavaScript对象,原型能存储我们的方法,构造函数创建出来的实例对象能够引用原型中的方法. 一.传统构造函数的问题 有如下代码 function Foo(){ this.sayHello = function(){ } } 由于对象是调用new Foo()所创建出来的,因此每一个对象在创建的时候,函数 sayHello 都会呗创建一次 那么有没一个对象都含有一个独立的,不

  • JS原型、原型链深入理解

    原型是JavaScript中一个比较难理解的概念,原型相关的属性也比较多,对象有"prototype"属性,函数对象有"prototype"属性,原型对象有"constructor"属性. 一.初识原型 在JavaScript中,原型也是一个对象,通过原型可以实现对象的属性继承,JavaScript的对象中都包含了一个"[[Prototype]]"内部属性,这个属性所对应的就是该对象的原型. "[[Prototype

  • JavaScript中原型和原型链详解

    javascript中的每个对象都有一个内置的属性prototype,Javascript中对象的prototype属性的解释是:返回对象类型原型的引用.意思是是prototype属性保存着对另一个JavaScript对象的引用,这个对象作为当前对象的父对象. 复制代码 代码如下: A.prototype = new B(); 理解prototype不应把它和继承混淆.A的prototype为B的一个实例,可以理解A将B中的方法和属性全部克隆了一遍.A能使用B的方法和属性.这里强调的是克隆而不是

  • js 原型对象和原型链理解

    一个例子让你彻底明白原型对象和原型链 开篇 之前对js中的原型链和原型对象有所了解,每当别人问我什么是原型链和原型对象时,我总是用很官方(其实自己不懂)的解释去描述.有一句话说的好:如果你不能把一个很复杂的东西用最简单的话语描述出来,那就说明你没有真正的理解.最近正在读<Javascript高级程序设计>,书中对原型对象和原型链的描述让我受益匪浅,下面仅用一个对比性的例子来说明. 我们经常会这么写 function Person () { this.name = 'John'; } var p

  • 深入理解JS继承和原型链的问题

    对于那些熟悉基于类的面向对象语言(Java 或者 C++)的开发者来说,JavaScript 的语法是比较怪异的,这是由于 JavaScript 是一门动态语言,而且它没有类的概念( ES6 新增了class 关键字,但只是语法糖,JavaScript 仍旧是基于原型). 涉及到继承这一块,Javascript 只有一种结构,那就是:对象.在 javaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接.这个原型对象又有自己的原型,直到某个对象的原型为null 为止

  • 几句话带你理解JS中的this、闭包、原型链

    原型链 所有对象都是基于Object.prototype,Object.prototype就是JavaScript的根对象,在Object.prototype中定义的方法都可以被其它对象访问到,当然也可以被重写了,所以直接在Object.prototype上调用的是原始功能的toString()方法,该方法会放回参数对象的内置属性[[class]]的值,这个值是个字符串,比如'[Object String]' 要理解原型链机制的话,首先得知道根本原因:JavaScript中的对象都有一个内置属性

  • JS原型和原型链原理与用法实例详解

    本文实例讲述了JS原型和原型链原理与用法.分享给大家供大家参考,具体如下: Javascript语言的继承机制一直很难被人理解. 它没有"子类"和"父类"的概念,也没有"类"(class)和"实例"(instance)的区分,全靠一种很奇特的"原型链"(prototype chain)模式,来实现继承. Brendan Eich设计javascript之初是为了实现网页与浏览器之间交互的一种简单的脚本语言

  • js原型链原理看图说明

    当初ECMAscript的发明者为了简化这门语言,同时又保持继承的属性,于是就设计了这个链表.. 在数据结构中学过链表不,链表中有一个位置相当于指针,指向下一个结构体. 于是乎__proto__也一样,每当你去定义一个prototype的时候,相当于把该实例的__proto__指向一个结构体,那么这个被指向结构体就称为该实例的原型. 文字说起来有点儿绕,看图说话 复制代码 代码如下: var foo = { x: 10, y: 20 }; 当我不指定__proto__的时候,foo也会预留一个这

随机推荐