JavaScript中的原型链

2023-02-23 0 340

责任编辑译者为奇舞蹈团前端开发技师

简述

JavaScript 是 Web 的编程词汇,单纯初学,使用方便,但由于过分灵巧结构设计经营理念,导致新手时常满脸懵,责任编辑要谈的是JavaScript中症结之一蓝本链。

蓝本链的今生

JavaScript的问世

要认知Javascript的蓝本链的结构设计思想,要从它的问世讲起。

1994年,Novell(Netscape)发布了Navigator应用程序0.9版。这是历史上第二个比较成形的互联网应用程序,彼时名噪一时。但这个版的应用程序根本无法用以下载,不具备与访客交互的能力。比如说,假如页面上有附注”使用者名”明确要求核对,应用程序就无法推论访客与否真的核对了,根本无法让应用程序将页面全部发回服务端,让服务器端推论与否核对。假如没有核对,服务器端就回到严重错误,明确要求使用者重新核对,这太无用和互联网资源了。

因此,Novell亟需一类页面脚本词汇,使得应用程序能与页面交互。技师Brendan Eich负责开发此种新词汇。他觉得,没必要性结构设计得很繁杂,此种词汇只要能够完成一些单纯操作方式就够了,比如说推论使用者是不是核对配置文件。

1994年正是面向第一类(object-oriented programming)最繁盛的时期,C++是彼时最盛行的词汇,而Java词汇的1.0版将要于第三年推出,Sun公司正在肆意摇旗呐喊。

Brendan Eich毫无疑问受到了影响,Javascript里头所有的正则表达式都是第一类(object) ,这一点与Java非常相近。但,他旋即就碰到了一个痛点,究竟要不要结构设计”承继”监督机制呢?

JavaScript是两门脚本词汇,是为了操作方式页面的,假如只将其作为固定式的脚本词汇,其实不需要有”继承”监督机制。但Javascript里头都是第一类,要有一类监督机制,能将第一类之间关连起来。所以Brendan Eich最后还是结构设计了”承继”。

但他不急于导入”类”(class)的概念,因为一旦有了”类”,Javascript就是一类完备的面向第一类词汇了,这好像有点太正式了,Brendan Eich考虑到C++和Java词汇都使用new指示聚合示例。

C++的读法是:

ClassName *object = new ClassName();

Java的读法是:

ClassName object = new ClassName();

因此,他把new指示导入了Javascript,用以从类(JavaScript中叫蓝本第一类)聚合一个示例第一类。但Javascript没有”类”,怎么来表示类(蓝本第一类)呢?

这时,他想到C++和Java使用new指示时,都会调用”类”的构造函数(constructor)。他就做了一个简化的结构设计,在Javascript词汇中,new指示后面跟的不是类,而是构造函数。

举例来说,现在有一个叫做Person的构造函数,表示人第一类的蓝本(能认知成java中的类)。

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

对这个构造函数使用new,就会聚合一个人第一类的示例。

var pA = new Person(老王); alert(pA.name); // 老王

注意构造函数中的this关键字,它就代表了新创建的示例第一类。

prototype属性的由来

对于面向第一类词汇比如说java或者c++来说,用构造函数聚合示例第一类是无法共享属性和方法,都有其独立的内存区域,互不影响。

比如说,在Person第一类的构造函数中,设置一个示例第一类的共有属性race。

function Person(name){ this.name = name; this.race = 汉族; }

然后,聚合两个示例第一类:

var pA = new Person(老王); var pB = new Person(老张);

这两个第一类的race属性是独立的,修改其中一个,不会影响到另一个。

pA.species = 苗族; alert(pB.species); // 显示”汉族”,不受pA的影响

每一个示例第一类,都有自己的属性和方法的副本。

按前面new运算符所述,每一个示例第一类,都有自己的属性和方法的副本。但此时假如我们想在同类但不同第一类间共享数据(承继)怎么办呢,解决此问题的方法就是prototype。

考虑到共享数据的问题,Brendan Eich决定为JavaScript的构造函数设置一个prototype属性。

这个属性包含一个第一类(以下简称”prototype第一类”),所有示例第一类需要共享的属性和方法,都放在这个第一类里头;那些不需要共享的属性和方法,就放在构造函数里头。这里prototype第一类有点像C++基类。

示例第一类一旦创建,将自动引用prototype第一类的属性和方法。也就是说,示例第一类的属性和方法,分成两种,一类是本地的,另一类是引用的。

还是以Person构造函数为例,现在用prototype属性进行改写:

function Person(name){ this.name = name; } Person.prototype = { race : 汉族 }; var pA = new Person(老王); var pB = new Person(老张); alert(pA.race); // 汉族 alert(pB.race); // 汉族

现在,race属性放在prototype第一类里,是两个示例第一类共享的。只要修改了prototype第一类,就会同时影响到两个示例第一类。

Person.prototype.race = 苗族; alert(pA.race); // 苗族 alert(pB.race); // 苗族

综上所述,由于所有的示例第一类共享同一个prototype第一类,那么从外界看起来,而示例第一类则好像”承继”了prototype第一类一样。

这就是Javascript承继监督机制的结构设计思想。

重写prototype属性、方法

用过java、c++类词汇的都知道,既然有承继,必然有重写。举例说明JavaScript的重写

Person.prototype.hairColor = black; Person.prototype.eat = function(){ console.log(Person eat) } console.log(pA) console.log(pB)

控制台输出:

JavaScript中的原型链
img

此时我们打印pA、pB,我们惊喜的发现,他们有了属性hairColor和eat方法;示例动态的获得了Person构造函数之后添加的属性、方法,这就是蓝本意义所在!能动态获取,这样能节省内存。

另外我们还要注意:假如pA将头发染成了黄色,那么hairColor会是什么呢?

pA.hairColor = yellow; console.log(pA) console.log(pB)

控制台输出:

JavaScript中的原型链
img

能看到,pA的hairColor = yellow, 而pB的hairColor = black;示例第一类重写蓝本上承继的属性、方法,相当于 “属性覆盖、属性屏蔽” ,这一操作方式不会改变蓝本上的属性、方法,自然也不会改变由统一构造函数创建的其他示例,只有修改蓝本第一类上的属性、方法,才能改变其他示例通过蓝本链获得的属性、方法。

承继与蓝本链

JavaScript 中一切皆第一类。每个示例第一类都有一个私有属性,称之为 proto,指向它的构造函数的蓝本第一类(prototype)。该蓝本第一类也有一个自己的蓝本第一类(proto),层层向上直到一个第一类的蓝本第一类为 null。根据定义,null 没有蓝本,并作为这个蓝本链中的最后一个环节。

蓝本链的经典图:
JavaScript中的原型链
img

这张图详细的描述了构造函数Function,Object以及它们示例之间的蓝本关系。

首先得记住并认知几个概念

属性proto是一个第一类,它有两个属性,constructor和proto;

蓝本第一类prototype有一个默认的constructor属性,用于记录示例是由哪个构造函数创建;

除了Object的蓝本第一类(Object.prototype)的proto指向null,其他内置函数的蓝本第一类和自定义构造函数的蓝本第一类的 proto都指向Object.prototype

Object.prototype.__proto__ === null;Array.prototype.__proto__ === Object.prototype;

结合一张pB第一类的结构图看就更容易认知了:

JavaScript中的原型链
img

创建第一类的方法

使用语法结构创建

var p = {name: “老三”}; // p 这个第一类承继了 Object.prototype 上面的所有属性 // Object.prototype 的蓝本为 null // 此第一类蓝本链如下: // p —> Object.prototype —> null var arr = [1,2,3,4,5]; // 数组都承继于 Array.prototype // 蓝本链如下: // arr —> Array.prototype —> Object.prototype —> null

使用构造器创建

在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作方式符 来作用这个函数时,它就能被称为构造方法(构造函数)。

function Animal(name,age) { this.name = name; this.age = age; } Animal.prototype.eat = function(){ console.log(Animal eat) }; var a = new Animal(“cat”,1); // a 是聚合的第一类,他的自身属性有 name 和 age。 // 在 a 被示例化时,a.__proto__ 指向了 Animal.prototype。

使用 Object.create 创建

ECMAScript 5 中导入了一个新方法:Object.create()。能调用这个方法来创建一个新第一类。新第一类的原型就是调用 create 方法时传入的第二个参数:

var a = {name: “老三”}; // a —> Object.prototype —> null var b = Object.create(a); // b —> a —> Object.prototype —> null console.log(b.name); // 老三 (承继而来) var c = Object.create(b); // c —> b —> a —> Object.prototype —> null var d = Object.create(null); // d —> null console.log(d.hasOwnProperty); // undefined,因为 d 没有承继 Object.prototype

class关键字

ECMAScript6 导入了一套新的关键字用以实现 class。使用java、swift等面向第一类的词汇的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript 仍然基于蓝本。这些新的关键字包括 class, constructor,static,extends 和 super。

class Animal { constructor(name) { this.name = name; } } class Person extends Animal { constructor(name,sex) { super(name); this.sex = sex; this.arrs = [1,2,3,4]; } } var pC = new Person(“老王”,”男”);

注意:类的本质还是一个函数,类就是构造函数的另一类读法,JavaScript 中并没有一个真正的 class 原始类型, class 、extends 仅仅只是对蓝本第一类运用语法糖,这样便于程序员认知。

function Person(){} console.log(typeof Person); //function class Person extends Animal { constructor(name,sex) { super(name); this.sex = sex; } get mysex() { return this.sex; } set mysex(sex) { this.sex = sex; } } console.log(typeof Person); //function

参考

轻松认知JS 蓝本蓝本链 https://juejin.cn/post/6844903989088092174

Javascript承继监督机制的结构设计思想 https://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html

承继与蓝本链 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务