为什么我们需要函数原型

398 阅读9分钟

此文章也于本人个人博客上发表了,博客上代码会看的更为清晰,博客指路:为什么我们需要函数原型 - Sheldon's blog (drsheldon-zh.com)

想要了解函数原型,就需要从JavaScript的对象继承模式说起(对象的继承:A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的)

大部分面对对象编程语言,如Java,其对象的继承基本都是通过类(class)来实现,而JavaScript则采用了原型对象(prototype)来得以实现

原型继承: 构造函数的实例对象自动拥有构造函数原型对象的属性和方法(利用原型链),这正是JavaScript实现继承的方法

ES6 引入了 class 语法,但此处先不做介绍,所以在JavaScript中,想要创建对象,有如下几种方法

对象创建方法

 //方法一:Object构造函数
 //适用场景:起始时不确定对象内部数据
 var p = new Object()
 p.name = 'jack'
 p.age = 18
 p.setName = function(name){
     this.name = name
 }
 ​
 //方法二:对象字面量模式
 //适用场景:起始时对象内部数据确定
 //多个相同对象时重复度高
 var p = {
     name: 'jack'
     age: 18
     setName:function(name){
         this.name = name
     }
 }
 ​
 //方法三:工厂函数动态创建对象并返回
 //适用场景:创建同样的多个对象
 //缺点:对象没有具体的类型,都是Object类型
 function createPerson(name,age){
     var p = {
         name: name
         age:age
         setName:function(name){
             this.name = name
         }
     }
     return p
 }
 ​
 //方法四:自定义构造函数
 //适用场景:创建多个类型确定的对象
 //每个对象都有相同的数据,浪费内存
 function Person(name,age){
     this.name=name;
     this.age=age;
     this.setName = function(name){
         this.name = name
     }
 }
  • 前两种方法都不能用于批量创建对象

  • 工厂函数和构造函数区别:

    • 创建实例化对象后,工厂函数实例化对象的constructor是Object,构造函数实例化对象的constructor是构造函数本身
    • 构造函数内部this是构造函数本身,并可以被继承到实例化对象上,工厂函数this指向window
    • 工厂函数和构造函数本身并没有区别(只是人为的喜欢将构造函数首字母大写),是因为new创建实例时产生的区别,稍后一点会详细讲解constructor和new

prototype

所有函数都有prototype属性,此处使用构造函数来进行解释

(在原型这一部分最好自己打开控制台调试,并且认真区分每个属性是函数还是对象)

 function Person(name){
     this.name=name;
     this.sayHi = function(){
         console.log('hi')
     }
 }
 ​
 var person1 = new Person('jack')
 var person2 = new Person('rose')
 person1.sayHi === person2.sayHi //false

由此我们可以看出:虽然都是同一个方法,但是实例化对象不同,导致他们并不相同,而是各自保存了自己的sayHi函数(具体原理在new中讲解),这就导致了内存的浪费

而为了解决这种内存的浪费,JavaScript就提出了原型(prototype)这个概念

既然每个实例化对象的相同方法或属性不同,那我们就为构造函数创建一个新的对象,名为prototype,专用于保存这种可以共享的属性或方法

 //于是上面的Person构造函数修改为
 function Person(name){
     this.name=name;
 }
 Person.prototype.sayHi = function(){
         console.log('hi')
     }
 ​
 //这样我们就可以通过原型来访问sayHi函数
 var person1 = new Person('jack')
 var person2 = new Person('rose')
 person1.sayHi === person2.sayHi //true
 //至于为什么person1就可以直接访问到sayHi函数,将在constructor中解释
 ​
 //此时如果我们console.log(person1)可以得到
 |--Person{name:'jack'}
  |--name: "jack"
  |--[[prototype]]:Object
   |--sayHi f()
   |--constructor: f Person(name)
   |--[[prototype]]: Object
 //可以看到此时Person的prototype就有了sayHi函数,下面的prototype是Object的原型,将在原型链中说到

constructor

所有对象都会从它的原型上继承一个 constructor 属性,用于指向此对象的构造函数constructor 属性即位于原型prototype上

 function Person(){
    
 }
 var p = new Person()
 console.log(p)// 打印出的p是一个Person对象
 |--Person{}
  |--[[prototype]]:Object
   |--constructor: f Person()
   |--[[prototype]]: Object
 //可以看到这个Person对象中就有一个prototype属性,而prototype中就保存有constructor,并且值为构造函数f Person()

所以我们为什么说是从原型上继承了constructor属性,因为p本身是没有这个属性,而是读取的构造函数上的原型的constructor 属性,即

 p.constructor === Person // true
 p.constructor === Person.prototype.constructor // true
 p.hasOwnProperty('constructor') // false
 ​
 //一般情况下,constructor用于索引对象的原型
 p.constructor.prototype

然后回到之前prototype中为什么person1.sayHi === person2.sayHi,其实可以理解为

 person1.constructor.prototype.sayHi === person2.constructor.prototype.sayHi
 //person1.constructor === Person

原型链

JavaScript 常被描述为一种基于原型的语言 (prototype-based language) ——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain) ,它解释了为何一个对象会拥有定义在其他对象中的属性和方法

这样的解释虽然很正确,但也非常官方,导致初学者可能难以理解

简单来说,JavaScript中每个函数都有一个原型prototype(其实对象也有,会在原型链原理中说到),用于索引其更上级的对象和函数

 function Person(){
 }
 var p = new Person()
 var obj = new Object()
 ​
 Person.prototype.x = 1;
 Object.prototype.y = 2;
 ​
 console.log(p)
 //此时形成的Person如下
 |--Person{}
  |--[[prototype]]:Object
   |--x:1
   |--constructor: f Person()
   |--[[prototype]]: Object
    |--y:2
    |--........
 //我们可以看到,之前例子中未知的prototype其实是Object的原型
 //而如果有构造函数Person2以Person为原型构造,则Person的prototype又是这个Person2的prototype上的prototype
 //由此,原型之间不断的连接形成了一种链式关系,就称为原型链
 ​
 console.log(p.y)
 //我们可以通过原型链直接去访问上层原型的属性或方法,如这里可以直接用p.y
 //这也是为什么我们可以直接用valueOf()等函数的原因,因为valueOf()位于Object的原型上

原型链有一些性质是我们必须记住的:

  • 原型链按顺序依次查找,如果上例中,有Person.prototype.y=3,则结果为3,因为找到后就不会再顺着原型链往上查找
  • 如果一层层地上溯,所有对象的原型都为Object。也就是说,所有对象都继承了Object.prototype的属性
  • 而Object的原型是null,因此,原型链的尽头就是null

原型链原理

关于原型链原理,其中的proto属性已经即将废除,但是目前还没有更好的方法来解释原型链原理,所以这部分也可以不用学习透彻,大概了解就行,实际上已经不会用到,但是对原型链的原理理解十分重要。

原型链实际上通过隐式原型查找,所以也成为隐式原型链

在实际的JavaScript工作中,并不是只有函数有prototype,每个实例化对象也会有一个隐式的prototype,为了区分,命名为proto

proto属性与prototype作用一样,都指向了构造函数的原型,只是proto是用于实例化对象

  • 每个构造函数都有一个prototype属性,默认指向一个空对象(除了Object构造函数),即显式原型
  • 每个实例对象都有一个proto属性,称为隐式原型
  • 隐式原型的值为其对应构造函数的显式原型的值
  • 实例对象中有一个constructor属性,指向构造函数
  • 构造函数和原型对象相互引用
 //上面的写成具体例子如下
 function Fn(){
     //默认this.prototype = {}
 }
 var f = new Fn()//内部语句f._proto_ = Fn.prototype
 //无法直接访问到f._proto_
 ​
 f._proto_ === Fn.prototype // true
 f.constructor === Fn === Fn.prototype.constructor // true
 ​
 //实际上原型链就变为
 Fn.prototype._proto_ === Object.prototype._proto_//这里Object是Fn的上层原型,不是具体指Object构造函数

所以实际上访问一个对象的属性的顺序为:

  • 先在自身属性中查找
  • 沿着proto链往上找,找到返回
  • 没找到就返回undefined

原型链还有如下几条性质:

  • function会new自身

    var Function = new Function()
    
  • Object并不是原型链顶层,Object.prototype才是原型链的顶层,有:Object.prototype.proto = null

  • 所有函数都是Function的实例(包括Function),有:Function.prototype = Function.proto

  • 所有对象都是Object的实例(包括原型对象),有:Function.prototype.proto = Object.prototype

  • 所有函数的proto属性都是一样,因为所有Function都new了自身

img

上图即为原型链完整原理,可以看出,确实十分的繁杂,所以目前基本只讨论prototype属性,而将proto封装起来

new

在前文所讲时,我都是用构造函数来举例,实际上普通函数和构造函数都有prototype,而造成普通函数和构造函数区别的,也正是new调用时,而不是函数创建时的写法

new 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 为步骤1新创建的对象添加属性proto,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this

如果不用proto,就解释为:

  1. 创建一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的原型,指向构造函数的prototype属性。
  3. 将这个空对象赋值给函数内部的this关键字。
  4. 开始执行构造函数内部的代码
//可以简单理解为如下代码
function New(){
    
}
var f = new New(){
    var this = {}
    this._proto_ = Fn.prototype
    //执行函数体内容
    
    return this
}
//如果构造函数return了其他的对象,则new命令会返回这个新对象
//如果构造函数return其他值,则不会返回,仍然返回this

//实际上new的实现代码会更为复杂
function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {
  // 将 arguments 对象转为数组
  var args = [].slice.call(arguments);
  // 取出构造函数
  var constructor = args.shift();
  // 创建一个空对象,继承构造函数的 prototype 属性
  var context = Object.create(constructor.prototype);
  // 执行构造函数
  var result = constructor.apply(context, args);
  // 如果返回结果是对象,就直接返回,否则返回 context 对象
  return (typeof result === 'object' && result != null) ? result : context;
}

// 实例
var actor = _new(Person, '张三', 28);

(前端的更新速度很快,可能此文所讲的一些原理,和所做的测试,在之后也会改变,如果部分测试结果不同,可能是由于浏览器版本问题,本文采用Google99.0.4844.51)

感谢观看!