八. 对象原型和函数原型

53 阅读5分钟

八. 对象原型和函数原型

8.1. 对象原型

  • JavaScript中每个对象都有一个特殊的内置属性 [[prototype]],只是我们看不到,这个特殊的对象可以指向另外一个对象。这个属性我们称之为对象的原型(隐式原型)

     var obj={name: "why"}
     // 和上面代码等同(只是一个模拟)  var obj={name: "why",__proto__:{} }
    

    补充:1. obj.__ proto __ = Object.prototype。(详情见 09原型链和继承 - 顶层原型是什么)

    2.所有对象的__ proto __属性默认都指向 创建该对象的函数的显示原型 , ​ 简记:对象是谁new的,proto就指向谁的显示原型

  • 那么这个对象有什么用呢?

    • 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作;

    • 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它;

    • 如果对象中没有该属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;(没有就继续往上找,直到找到Object,后面会讲到)

       var obj = { name: "why" }
       obj.__proto__.age = 18
       Object.getPrototypeOf(obj).address="北京市"
       ​
       console.log(obj.name); // why
       console.log(obj.age); // 18
       console.log(obj.address); // "北京市"
      
  • 那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?

  • 答案是有的,只要是对象都会有这样的一个内置属性;

  • 获取的方式有两种:

    • 方式一:通过对象的 _ _ proto _ _ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);

    • 方式二:通过 Object.getPrototypeOf 方法可以获取到;(早期的ECMA是没有规范如何去查看[[prototype]],该方法是ES5之后提供的)

       var obj= {}
       console.log(obj.__proto__);
       console.log(Object.getPrototypeOf(obj));
      

8.2. 函数原型

  • 问:那么我们知道上面的东西对于我们的构造函数创建对象来说有什么用呢?

    • 它的意义是非常重大的,接下来我们继续来探讨;但需要先了解函数的prototype

这里我们又要引入一个新的概念:prototype属性(js内置属性,不存在兼容)

 function Foo(){ }  // 下面以Foo为例
  • 所有的函数都有一个prototype显示原型:Foo.prototype

    • prototype来自哪里?

    • 答:一旦创建一个函数,js引擎会自动帮我们创建一个对象,并将Foo的prototype属性指向这个对象。

      • Foo.prototype = { constructor: Foo }
  • 所有的对象都有一个[[prototype]] 隐式原型: Foo.__ proto __

    • __ proto __ 来自哪里?

    • 答:来自new Function(), new操作时js引擎会自动把Foo.__ proto __ 赋值为Function.prototype。

      • Foo.__ proto __ = Function.prototype
    • Function.prototype来自哪里?

    • 答:在创建Function函数的时候,引擎给它创建的,并将Function的prototype属性指向这个对象。

      • Function.prototype = { constructor: Function }

下面是验证代码

   function Foo() {}
   console.log(Foo.prototype.constructor); // [Function: Foo]
   console.log(Foo.__proto__.constructor); // [Function: Function]
  • 那么这个prototype有什么用呢?(作用在于new对象时的第二个步骤)

    • 当使用new创建对象时,这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
    • 自己的总结:将函数的显示原型 赋值 给对象的隐式原型
     // 下面是模拟代码
     function foo() {
         // 1.创建一个空对象
         var moni = {}
         // 2.将函数的prototype赋值给空对象的__proto__
         moni.__proto__ = foo.prototype
     }
    
     // 下面是验证代码
     function foo() {
     }
     let f1=new foo()
     let f2=new foo()
     console.log(f1.__proto__ === foo.prototype); // true
     console.log(f1.__proto__ === f2.__proto__); // true
     console.log(f2.__proto__ === foo.prototype); // true
    

Tips:

  1. Foo的显示原型和隐式原型不是一个东西,(Function 函数例外)

     Foo.prototype === Foo.__ proto __;    // false  Foo的隐式原型 ≠ 显示原型
    
  2. 为什么函数是对象?

    假如让我们封装一个函数,用于计算两数相加的和,有以下三种方式:

    • 前面两种实际都是第三种的语法糖,定义函数时候都会通过 new Function 来创建一个函数,只是前两种为我们进行了封装,既然sum是new Function创建的,那么sum.__ proto __ = Function.prototype
     // 第一种
     function sum(a, b) { return a + b }
     // 第二种,
     let sum1 = (a, b) => a + b;
     // 第三种
     let sum2 = new Function('a', 'b', 'return a + b');
     sum2(1, 2)
    

注意:不是因为函数是一个对象,所以它才有prototype的属性,是因为它本身就是一个函数,所有函数都有这个特殊的属性;而不是因为它是一个对象,所以有这个特殊的属性;

8.3. 创建对象的内存表现

 function Person() {
 }
 let p1=new Person()
 let p2=new Person() 
 console.log(p1.name)

image-20220223155743405.png

执行p1.name时,现在自己的对象(p1对象)里面找,没有找到再去Person函数的原型对象找,也没有找到,最后返回undefined,

如果想给p1对象添加一个name属性,下面四个方法都可以

 p1.name="a"  
 p1.__proto__.name="b"  
 Person.prototype.name="c"
 p2.__proto__.name="d"  

8.4. 函数原型上的属性

  • 事实上原型对象上面是有一个属性的:constructor

    • 默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象;并且enumerable是false

下面是foo函数的原型对象 和 原型对象的数据属性image-20220223204919541

我们也可以修改foo原型对象的数据属性

image-20220223204919541.png

 function foo() {}
 Object.defineProperty(foo.prototype,"constructor",{
     configurable:true,
     enumerable:true,
     writable:true,
     value:"hahaha"
 })
 console.log(foo.prototype); // {constructor: 'hahaha'}

8.9. 重写原型对象

如果我们想给foo原型对象添加多个属性,可以像下面那样做,但代码会很冗余

 function foo() {}
 foo.prototype.name="why"
 foo.prototype.age="18"
 foo.prototype.height="180"

这时候我们可以直接修改整个prototype对象,

  • 当prototype赋值为一个新对象时,prototype会指向这个新对象,
  • 然后给对象添加name, age, height属性,但我们创建的这个新对象还没有constructor,
  • 不支持直接在prototype里面直接添加constructor,因为我们还要对它的数据属性做一些控制,用Object.defineProperty添加constructor后,我们新创建的对象和原来的prototype对象就完全一致了

修改完后,再通过new foo()创建出来的对象的[[Prototype]]就是指向我们创建的新对象了,而之前的0x001原型对象会被回收

 foo.prototype={
     // constructor: foo  不支持这样写,因为还要设置constructor的数据属性
     name:"why",
     age:18,
     height:180
 }
 Object.defineProperty(foo.prototype, "constructor", {
     configurable: true,
     enumerable: false,
     writable: false,
     value: foo
 })

image-20220223220807157.png