一篇带你了解对象原型

119 阅读8分钟

认识对象的原型

JavaScript当中每个对象都有一个特殊的内置属性prototype,这个特殊的对象可以指向另外一个对象

image.png

如何获取获取的方式有两种:

  • 方式一:通过对象的 proto 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);
  • 方式二:通过 Object.getPrototypeOf 方法可以获取到,这个是官方给的方式;
    console.log(obj.__proto__)
    console.log(Object.getPrototypeOf(obj))
    console.log(obj.__proto__ === Object.getPrototypeOf(obj)) // true 

image.png

原型有什么用呢?

  • 当我们通过[[get]]方式获取一个属性对应的value时

[[get]]方式就是.访问,对象不仅可以通过.访问属性,也可以通过[]访问。 例如obj.nameobj[name]是一样的

  • 首先它会优先在自己的对象中查找, 如果找到直接返回
  • 如果没有找到, 那么会在原型对象中查找

例如

    console.log(obj.message)
    obj.__proto__.message = "Hello World"
    console.log(obj.message)

image.png 开始obj自身和它原型里面都是没有message属性的,所以第一次会打印出undefined,然后我们给它的原型赋值了message,第二次打印的时候,它本身没有message这个属性,然后就会去原型里面查找,原型里面这个时候有message,最后打印出Hello World。

函数的原型 prototype

这里我们要引入一个新的概念:所有的函数都有一个prototype的属性(注意:不是__proto__

    var obj = {}
    function foo() {}
    console.log(obj.__proto__)
    console.log(foo.__proto__)

将函数看成一个对象(函数在 JavaScript 中也是一种特殊的对象)的时候,它是具备__proto__(隐式原型)的,其作用就是查找key对应的value时, 会找到原型身上

你可能会想是否是因为函数是一个对象,所以它有prototype的属性呢?

不是的,因为它是一个函数,才有了这个特殊的属性,而不是它是一个对象,所以有这个特殊的属性

    console.log(foo.prototype)
    console.log(obj.prototype) //对象是没有prototype

image.png

那显示原型有什么用呢?🤔

作用: 用来构建对象时, 给对象设置隐式原型的 相比我们之前都学过new关键字 我们来回顾一下new的作用

这里new干了什么呢🤔?

假设var obj = new Person()

  • 1.创建空对象var obj= {}
  • 2.将这个空对象赋值给this this =obj
  • 3.将函数的显式原型赋值给这个对象作为它的隐式原型 obj. proto_= Person.prototype
  • 4.执行函数体中代码
  • 5.将这个对象默认返回 这里我们重点关注第三点,new一个对象出来的时候将函数的显式原型赋值给这个对象作为它的隐式原型
 function Foo() {
      // 1.创建空的对象
      // 2.将Foo的prototype原型(显式隐式)赋值给空的对象的__proto__(隐式原型)
    }

    console.log(Foo.prototype)

    var f1 = new Foo()
    var f2 = new Foo()
    var f3 = new Foo()
    var f4 = new Foo()
    var f5 = new Foo()
    console.log(f1.__proto__)
    console.log(f1.__proto__ === Foo.prototype) // true
    console.log(f3.__proto__ === f5.__proto__) // true

image.png 可以发现此时所有new出来对象的隐式原型都是函数的显示原型。

用这个性质我们可以干什么?🤔

这种好处可大了,我们可以将函数里面的方法放在原型上!!!🤩 我们来看这段代码

function Student(name, age, sno) {
      this.name = name
      this.age = age
      this.sno = sno

      this.running = function () {
        console.log(this.name + " running")
      }
      this.eating = function () {
        console.log(this.name + " eating")
      }
      this.studying = function () {
        console.log(this.name + " studying")
      }
    }
    var stu1 = new Student("why", 18, 111)
    var stu2 = new Student("kobe", 30, 112)
    var stu3 = new Student("james", 18, 111)

我们可以看看这段代码有什么缺陷吗?🤔

显然,当我们每次创建一个对象的时候,里面的所有function都会创建一遍,非常浪费内存。

image.png 那我们有什么解决方法吗?没错,这里就是要用到显示原型了。

    Student.prototype.running = function() {
      console.log(this.name + " running")
    }
    Student.prototype.eating = function() {
      console.log(this.name + " eating")
    }
    Student.prototype.studying = function() {
      console.log(this.name + " studying")
    }

补充一下,为什么能Student.prototype.running这么写

因为Student.prototype本质也是一个对象

console.log(Student.prototype) //Object

image.png stu1的隐式原型是谁? 是Student.prototype对象,因为new的时候会将函数的显式原型赋值给这个对象作为它的隐式原型

stu1.running查找: 1、 先在自己身上查找, 没有找到, 2.去原型去查找。 这里stu1自身是没有runing函数的,但是我们给Student这个对象的原型上添加了running函数,所以去原型上找的时候找到了。 不用担心这里this绑定问题,因为我们是通过对象调用的,会进行隐式绑定,这里的this就是调用者stu1

总结:当我们多个对象拥有共同的值时, 我们可以将它放到构造函数对象的显式原型,由构造函数创建出来的所有对象, 都会共享这些属性

原型上的const

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

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

  function Person() {

    }

    // 1.对constructor在prototype上的验证
    var PersonPrototype = Person.prototype
    console.log(PersonPrototype)
    console.log(PersonPrototype.constructor)
    console.log(PersonPrototype.constructor === Person)

    console.log(Person.name)
    console.log(PersonPrototype.constructor.name)

image.png 通过这些代码我们可以得出一个结论:

原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象,他们俩共用一个内存地址

如果不能理解,我们这里通过内存来理解,这里如果看懂了,你将进入一个新境界🚀

我们来看这串代码

function Person(name, age) {
      this.name = name
      this.age = age
    }

    Person.prototype.running = function() {
      console.log("running~")
    }

    var p1 = new Person("why", 18)
    var p2 = new Person("kobe", 30)

在内存里面的表现是什么样的呢?我这里画了一张图。灵魂画家😄 image.png 这里来简单概述一下这张图:

  • p1,p2是Person实例化出来的对象,所以Person函数的显式原型赋值给这个对象作为它们的隐式原型。所以_proto_即[[Prototype]]指向内存地州的是Person的原型对象的内存地址。
  • 因为每个函数都有constructor,这个constructor存在于函数的原型对象中,而constructor又指向当前的函数,所以他们俩共用一个内存地址
  • 给Person原型上面添加了一个running函数,所以running函数的内存地址指向Person原型对象中的running函数地址。

我们现在改变一下。


    function Person(name, age) {
      this.name = name
      this.age = age
    }

    Person.prototype.running = function() {
      console.log("running~")
    }

    var p1 = new Person("why", 18)
    var p2 = new Person("kobe", 30)

    // 进行操作
    console.log(p1.name)
    console.log(p2.name)

    p1.running()
    p2.running()

    // 新增属性
    Person.prototype.address = "中国"
    p1.__proto__.info = "中国很美丽!"

    p1.height = 1.88
    p2.isAdmin = true

这里我们新增了属性,内存图是怎么样的呢?可以自己动手画画。 灵魂画手帮你们画了🕶

image.png 我们来解释一下新增的属性

  • Person.prototype.address = "中国",这个很好理解,就是在Person原型对象上加了address
  • p1.proto.info = "中国很美丽!",因为p1的隐式原型就是Person原型,所以本质也就是在Person原型上加了info。
  • p1.height = 1.88,这里没有操作原型,操作的是p1这个对象,所以是在p1对象上加了height属性
  • p2.isAdmin = true,这里没有操作原型,操作的是p2这个对象,所以是在p2对象上加了isAdmin属性

我们此时来获取属性

    console.log(p1.address)
    console.log(p2.isAdmin)
    console.log(p1.isAdmin)
    console.log(p2.info)

自己想一想打印出的值是什么?🤔

答案是

image.png

  • p1.address,首先会在当前对象p1里面找是否存在address的,发现不存在,然后会去当前对象的原型对象找,因为p1对象的原型对象就是Person原型对象,而Person原型对象中有address,所以最后打印出"中国"。
  • p2.isAdmin,首先会在当前对象p2里面找是否存在address,发现刚好有,所以直接打印出true。
  • p1.Admin,首先会在当前对象p1里面找是否存在isAdmin的,发现不存在,,然后会去当前对象的原型对象找,因为p1对象的原型对象就是Person原型对象,但是Person原型对象中也没有isAdmin,所以最后打印出undefined。
  • p2.info,首先会在当前对象p2里面找是否存在info的,发现不存在,然后会去当前对象的原型对象找。因为p2对象的原型对象就是Person原型对象,而Person原型对象中有info,所以最后打印出"中国跟美丽"。

👌,我们继续改动,


    function Person(name, age) {
      this.name = name
      this.age = age
    }

    Person.prototype.running = function() {
      console.log("running~")
    }

    var p1 = new Person("why", 18)
    var p2 = new Person("kobe", 30)
    // 新增属性
    Person.prototype.address = "中国"
    p1.__proto__.info = "中国很美丽!"

    p1.height = 1.88
    p2.isAdmin = true

    // 获取属性
   // console.log(p1.address)
   // console.log(p2.isAdmin)
   // console.log(p1.isAdmin)
   // console.log(p2.info)

    // 修改address
    p1.address = "广州市"
    console.log(p2.address)

这里我们修改了address

     p1.address = "广州市"
    console.log(p2.address)

猜下会打印出什么?🤔 答案是“中国”,因为我们这里是p1.address,只是在p1对象里面新增了address,并没有操作原型里面的address,p2最后访问依然原来的。

函数原型对象赋值新对象

Person.prototype.message = "Hello Person"
    Person.prototype.info = { name: "哈哈哈", age: 30 }
    Person.prototype.running = function() {}
    Person.prototype.eating = function() {}
Person.prototype = {
      message: "Hello Person",
      info: { name: "哈哈哈", age: 30 },
      running: function() {},
      eating: function() {},
    }

上面两个方法都能给Person原型对象添加属性,很明显,第二种更方便,但是第二种有什么缺陷呢?🤔 我们发现第二种原型少了constructor。 所以我们常常给它加上一个

Person.prototype = {
      message: "Hello Person",
      info: { name: "哈哈哈", age: 30 },
      running: function() {},
      eating: function() {},
      constructor: Person
    }

但这样就完美了吗?🤔 我们先来看看第一种

    console.log(Person.prototype)
    console.log(Object.keys(Person.prototype))

image.png 我们这里发现浏览器默认下的constructor是不会出现在Object.keys中的,而constructor的颜色也与其他不一样。 那我们看看第二种打印出来的

image.png 发现这里的constructor的颜色前面的属性一致,并且也出现了在了Object.keys中,而且也会造成constructor的[[Enumerable]]特性被设置了true.。

默认情况下, 原生的constructor属性是不可枚举的.

如果希望解决这个问题, 就可以使用我们前面介绍的Object.defineProperty()函数了.。

Object.defineProperty(Person.prototype, "constructor", {
      enumerable: false,
      configurable: true,
      writable: true,
      value: Person
    })

image.png 这时候就一样了。