简介:也许你听说过,或看到过prototype这个属性,又或者是在某篇技术贴中见过原型、隐式原型、原型链这种说法,它们究竟是什么?有什么用?跟面向对象有什么关系?在这一章,我们将一步步深入JS语言,去了解这些晦涩难懂 ...
也许你听说过,或看到过prototype这个属性,又或者是在某篇技术贴中见过原型、隐式原型、原型链这种说法,它们究竟是什么?有什么用?跟面向对象有什么关系?在这一章,我们将一步步深入JS语言,去了解这些晦涩难懂的概念。当你搞清楚了这些概念后,你就能够理解JS语言中一些怪异的现象,并且在使用对象和函数时,有足够的信心和能力控制它。 函数即对象先来看一段简单的代码: //一个空空如也的函数function nothing(){}console.log(nothing.name); //输出:nothingconsole.log(nothing.length); //输出:0console.log(nothing.apply); //输出:function 这段代码的语法非常奇怪,nothing明明是一个函数,可是,在后面几句代码中,看上去是将它当作对象在使用,输出nothing对象的三个属性name、length、apply。 代码没有报错,居然还输出了结果! 这里就出现了两个问题:
这两个问题,实际上就涵盖了本章的所有知识。 别着急,我们先来回答第一个问题:函数怎么能当作是对象来使用呢? 答案简单而又疯狂:因为函数本身就是对象! 在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函数,就可以创建各种各样的函数,然后通过函数创建各种各样的对象了。 现在,我们再来回顾之前的问题:
第一个问题已经回答了。 至于第二个问题,需要看完本章所有的内容后才能得到答案(篇幅有点长,注意中途休息)。 原型(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 这一部分特别绕。我当初理解这部分花费了两天的时间,相信你们比我聪明。只有明白了这一部分,才能解答一些怪异的面试题。 于是,这样一来,就形成了原型、隐式原型的全貌图: 上图信息量巨大,需要长时间观察梳理才能彻底理解! 如果你观察足够仔细,会发现,上图中还缺失了两个东西:
始终记住,对象的隐式原型,始终指向创建它的构造函数的原型 可是,Function是JS引擎直接放置到内存的,不通过构造函数产生;Object的原型本身就是靠Object函数创建的,怎么办呢? 这是隐式原型中两个特殊的地方,因此JS对它们进行了特殊处理:
让我们把这幅图画完整: 如果你理解了上图,不仅能够在操作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); 此题的答案我不会告诉你,请你自行思考和试验,如果结果和你想的不一致,那你需要继续看上面的结构图。 总结
关于原型、隐式原型,还有一些知识,我们将放到下一章讲解。本文仅代表作者个人观点,不代表巅云官方发声,对观点有疑义请先联系作者本人进行修改,若内容非法请联系平台管理员,邮箱2522407257@qq.com。更多相关资讯,请到巅云www.yinxi.net学习互联网营销技术请到巅云建站www.yx10011.com。 |