JS面向对象系列教程—原型
author:一佰互联 2019-03-30   click:167

简介:也许你听说过,或看到过prototype这个属性,又或者是在某篇技术贴中见过原型、隐式原型、原型链这种说法,它们究竟是什么?有什么用?跟面向对象有什么关系?在这一章,我们将一步步深入JS语言,去了解这些晦涩难懂 ...

也许你听说过,或看到过prototype这个属性,又或者是在某篇技术贴中见过原型隐式原型原型链这种说法,它们究竟是什么?有什么用?跟面向对象有什么关系?在这一章,我们将一步步深入JS语言,去了解这些晦涩难懂的概念。当你搞清楚了这些概念后,你就能够理解JS语言中一些怪异的现象,并且在使用对象和函数时,有足够的信心和能力控制它。

JS面向对象系列教程—原型

函数即对象

先来看一段简单的代码:

//一个空空如也的函数function nothing(){}console.log(nothing.name); //输出:nothingconsole.log(nothing.length); //输出:0console.log(nothing.apply); //输出:function

这段代码的语法非常奇怪,nothing明明是一个函数,可是,在后面几句代码中,看上去是将它当作对象在使用,输出nothing对象的三个属性name、length、apply。

代码没有报错,居然还输出了结果!

这里就出现了两个问题:

  1. 函数怎么能当作是对象来使用呢?
  2. 就算函数是对象,那代码中的那三个属性是哪来的呢?我可没有去创建它们。

这两个问题,实际上就涵盖了本章的所有知识。

别着急,我们先来回答第一个问题:函数怎么能当作是对象来使用呢?

答案简单而又疯狂:因为函数本身就是对象!

在JS中,函数本质就是对象,我们姑且把它叫做函数对象,它跟其他对象唯一的不同,就是创建函数对象的构造函数不一样

经过之前的知识,我们知道,所有的对象都是通过new产生的,也就是说,是通过构造函数产生的。

那么既然函数也是对象,它也必须通过构造函数产生咯?

是的,函数对象和其他对象的区别,就仅仅在于产生函数的构造函数,和其他对象不一样。

那产生函数对象的构造函数是什么呢?是一个叫做Function的特殊函数,通过new Function产生的对象就是一个函数。

下面的代码说明了这一点:

function nothing() {}//上面的函数等同于:var nothing = new Function();function add(a, b) { return a + b;}//上面的函数等同于:var add = new Function("a", "b", "return a + b");//前面的是函数形参名,最后一个参数是函数体

可见,跟{}创建对象一样,书写一个函数的语法仍然是一个语法糖,要创建一个函数,真正的代码是new一个Function的对象!

你可以在代码中试试用新的形式创建函数,然后调用它,看能不能完成函数的功能。

也就是说:只要通过Function创建的对象就是函数!函数都是通过Function创建的,本质是对象!

从上图中可以看出,不仅我们自己写的函数是通过Function创建的,就连那些JS内置的函数也是通过Function创建的。

再结合之前的知识,通过Function函数创建的对象就是函数,而函数也可以用来创建对象,于是,我们就得到了下面这张关系图

上面的图你要多看一会儿,确保自己理解了图中的关系,再进行下面的学习。

在这张图中,有一个非常特殊的函数,就是位于中心的Function,它当然也是一个函数,同时也是一个对象,可是它是从哪里来的呢?是通过new哪个函数创建的呢?Function的特殊性就在这里,Function函数不通过其他函数得到,它是JS执行引擎在初始化时就直接通过本地代码(C或C++)直接放置到内存中的。有了Function函数,就可以创建各种各样的函数,然后通过函数创建各种各样的对象了。

现在,我们再来回顾之前的问题:

  1. 函数怎么能当作是对象来使用呢?
  2. 就算函数是对象,那代码中的那三个属性是哪来的呢?我可没有去创建它们。

第一个问题已经回答了。

至于第二个问题,需要看完本章所有的内容后才能得到答案(篇幅有点长,注意中途休息)。

原型(prototype)

既然函数就是对象,那么函数自然可以拥有一些对象的成员。其中,原型(prototype)就是所有函数与生俱来的成员。

所有函数创建后,创建的函数对象都会自动的附带一个属性prototype,它是一个Object对象,代表着函数的原型

所有函数创建后,创建的函数对象都会自动的附带一个属性prototype,它是一个Object对象,代表着函数的原型

所有函数创建后,创建的函数对象都会自动的附带一个属性prototype,它是一个Object对象,代表着函数的原型

上面的每一个字都非常重要!

你可以想象在Function的代码中,大概是这种格式:

function Function(...args) { this.prototype = { ... }; //给函数对象创建prototype属性 //其他语句}

这样一来,每个函数创建后,都自动的拥有了属性prototype。

prototype叫做原型,可是这个原型有什么用呢?目前很难解释它的用处,不要着急,我们先认识它,后文会涉及到它的用处。

现在,我们先来看看,这个原型对象中有什么东西

从上图中,可以看出原型对象中其实没啥东西,就两个成员:constructor和__proto__。

你可以试试看,往原型里面加入一些成员,然后打印它们。

目前,我们仅需认识一下这个constructor就够了,至于__proto__则交给本章后文讲解。

constructor这个属性是指创建原型的函数,它指向函数本身,于是有了下面的关系

因此,下面的等式是永远成立的:

function nothing() {}console.log(nothing.prototype.constructor === nothing); //输出:true

由于原型中的constructor对我们本章的知识没有多大意义,因此作为了解即可。

接下来,我们的重头戏,就是来介绍原型中的__proto__属性。

始终记住,每个函数都有prototype属性,而prototype属性仅存在于函数中,不是函数的对象是没有这个属性的。例如,console.log({}.prototype);将输出undefined。

隐式原型(__proto__)

截至目前,我们仍然不知道原型(prototype)的意义在哪,那是因为我们的拼图中还缺少一块,就是隐式原型(__proto__)。

所有的对象都包含一个属性__proto__,它指向创建该对象的构造函数的原型

是不是贼绕?是的,就是这么绕,但立志成为前端工程师的你,必须要搞清楚!

下面这张图可以帮助你理清楚它们之间的关系:

这些乱七八糟的关系到底有什么用?

别急,别急,别急,快接近答案了。

完成下面这道练习题,看自己是否理解了它们之间的关系:

function Nothing() {}const obj1 = new Nothing();const obj2 = new Nothing();console.log(obj1 === obj2); //输出:?console.log(obj1.prototype); //输出:?console.log(obj1.__proto__ === obj2.__proto__); //输出:?console.log(obj1.__proto__ === Nothing.prototype); //输出:?

你是否能说出答案?

答案如下:

function Nothing() {}const obj1 = new Nothing();const obj2 = new Nothing();//两个对象的地址不同console.log(obj1 === obj2); //输出:false//原型只有函数才有,obj1是普通对象,没有原型console.log(obj1.prototype); //输出:undefined//obj1和obj2都是通过Nothing函数创建的,它们的隐式原型都指向Nothing函数的原型,因此相同console.log(obj1.__proto__ === obj2.__proto__); //输出:true//obj1是通过Nothing函数创建的,它的隐式原型指向Nothing函数的原型,因此相同console.log(obj1.__proto__ === Nothing.prototype); //输出:true

确保你能看懂上面的答案,然后再继续下面的学习。

接下来,我就要解开这一切的意义了。

从上一张图中,我们可以看出:由同一个函数产生的对象,它们的隐式原型,都指向函数的原型

例如:图中的对象1和对象2的隐式原型,都指向函数add的原型。

这样一来,函数原型中保存的东西,就可以被所有对象共享了

下面来一段有意义的代码:

function User(name, age) { this.name = name; this.age = age; this.sayHello = function() { console.log(`你好,我叫${this.name},今年${this.age}岁了`); }}const u1 = new User("bangbangji", 18);const u2 = new User("xiaobaitu", 16);u1.sayHello(); //输出:你好,我叫bangbangji,今年18岁了u2.sayHello(); //输出:你好,我叫xiaobaitu,今年16岁了console.log(u1.sayHello === u2.sayHello); //输出:false

由于u1和u2中的sayHello成员,是附着在各自的对象上的,因此程序的最后一行输出了false,因为它们虽然功能相同,但实际上不是同一个东西,如下图所示:

但是这样做没有什么意义!因为sayHello作为函数,无论属于哪一个对象,功能都是一样的!让其在每个对象中出现,除了浪费内存空间,没有任何其他意义。

想象一下,如果我们创建了100个用户对象,岂不是跟着创建了100个sayHello函数?

于是,我们将程序修改一下:

function User(name, age) { this.name = name; this.age = age;}User.prototype.sayHello = function() { console.log(`你好,我叫${this.name},今年${this.age}岁了`);}const u1 = new User("bangbangji", 18);const u2 = new User("xiaobaitu", 16);u1.__proto__.sayHello(); //输出:你好,我叫bangbangji,今年18岁了u2.__proto__.sayHello(); //输出:你好,我叫xiaobaitu,今年16岁了console.log(u1.__proto__.sayHello === u2.__proto__.sayHello);//输出:true

现在,我们把sayHello放到了User原型里面,之后凡是通过User创建的对象,都可以通过隐式原型__proto__找到sayHello函数,所有对象共享该函数的功能。

现在对象的结构变成了下图所示:

现在,所有通过函数User创建的对象,都共享了原型中的成员sayHello,无论你创建多少个User对象,都不会导致额外内存的开销。

只不过….这样写代码实在是太麻烦了!

好在JS在语法上提供了这样的功能:当读取对象的成员时,若对象自身没有该成员,将会从对象的隐式原型中查找!

sayHello虽然不是对象的成员,但它属于对象的隐式原型!JS会自动帮我们到隐式原型中查找,因此代码可以简化为:

//虽然u1和u2没有sayHello的成员,但它们的隐式原型中有,因此让JS引擎自己去找吧u1.sayHello(); //输出:你好,我叫bangbangji,今年18岁了u2.sayHello(); //输出:你好,我叫xiaobaitu,今年16岁了console.log(u1.sayHello === u2.sayHello); //输出:true

由于对象的方法功能都是一样的,并不存在差异。因此:

我们可以将对象的方法放置到构造函数的原型中,减少内存的开销

我们可以将对象的方法放置到构造函数的原型中,减少内存的开销

我们可以将对象的方法放置到构造函数的原型中,减少内存的开销

这就是原型、隐式原型的重要意义!

链条的全貌

事情变得有趣而复杂起来。

既然所有的对象都有隐式原型(__proto__),那函数也是对象,原型也是对象,它们也具有隐式原型咯?

是的!所有对象都有隐式原型!

那函数的隐式原型、原型的隐式原型是什么呢?

回忆一下隐式原型的指向:隐式原型指向创建该对象的构造函数的原型

而函数是由Function创建的,因此函数的隐式原型应该指向Function的原型:

console.log(Object.__proto__ === Function.prototype); //输出:true

那原型本身呢?

原型是通过Object创建的,因此,原型对象的隐式原型指向的是Object的原型:

console.log(Function.prototype.__proto__ === Object.prototype); //输出:true
这一部分特别绕。我当初理解这部分花费了两天的时间,相信你们比我聪明。只有明白了这一部分,才能解答一些怪异的面试题。

于是,这样一来,就形成了原型、隐式原型的全貌图:

上图信息量巨大,需要长时间观察梳理才能彻底理解!

如果你观察足够仔细,会发现,上图中还缺失了两个东西:

  1. Function也是对象,它的隐式原型指向谁呢?
  2. Object的原型也是对象,它的隐式原型指向谁呢?

始终记住,对象的隐式原型,始终指向创建它的构造函数的原型

可是,Function是JS引擎直接放置到内存的,不通过构造函数产生;Object的原型本身就是靠Object函数创建的,怎么办呢?

这是隐式原型中两个特殊的地方,因此JS对它们进行了特殊处理:

  1. Function的隐式原型指向自身原型
  2. Object的隐式原型指向null

让我们把这幅图画完整:

如果你理解了上图,不仅能够在操作JS原型时得心应手,并且能够应付各种关于原型的面试题。

下面,我们以一道面试题结束本章。

阅读并思考下面的程序输出什么结果:

function User() {}User.prototype.sayHello = function() {}const u1 = new User();const u2 = new User();console.log(u1.sayHello === u2.sayHello);console.log(u1.prototype); console.log(User.prototype === Function.prototype);console.log(User.__proto__ === Function.prototype);console.log(User.__proto__ === Function.__proto__);console.log(u1.__proto__ === u2.__proto__);console.log(u1.__proto__ === User.__proto__);console.log(Function.__proto__ === Object.__proto__);console.log(Function.prototype.__proto__ === Object.prototype.__proto__);console.log(Function.prototype.__proto__ === Object.prototype);
此题的答案我不会告诉你,请你自行思考和试验,如果结果和你想的不一致,那你需要继续看上面的结构图。

总结

  1. 所有的函数都是由new Function创建的,Function本身除外,它是直接被放置到内存中的
  2. 所有的函数都有一个属性prototype(原型)
  3. 所有的对象都有一个属性__proto(隐式原型),该属性指向创建该对象的构造函数的原型。这意味着同一个构造函数产生的对象,它们共享该函数的原型。
  4. Object原型的隐式原型为null;Function的隐式原型为自身的原型。这是两个特殊点。
  5. 在使用对象属性时,若自身属性不存在,JS会自动从隐式原型中寻找。
  6. 在编写构造函数时,我们应该把对象的方法放到函数的原型中,以避免内存空间的浪费。
关于原型、隐式原型,还有一些知识,我们将放到下一章讲解。
本文仅代表作者个人观点,不代表巅云官方发声,对观点有疑义请先联系作者本人进行修改,若内容非法请联系平台管理员,邮箱2522407257@qq.com。更多相关资讯,请到巅云www.yinxi.net学习互联网营销技术请到巅云建站www.yx10011.com。