对象补充-原型和函数原型-创建对象

275 阅读6分钟

跟随coderwhy学习

1.创建多个对象的方案

  • 如果我们现在希望创建一系列的对象:比如Person对象
    • 包括张三、李四、王五、李雷等等,他们的信息各不相同;
    • 那么采用什么方式来创建比较好呢?
  • 目前我们已经学习了两种方式:
    • new Object方式;

    • 字面量创建的方式;

image.png

  • 这种方式有一个很大的弊端:创建同样的对象时,需要编写重复的代码;

2.创建对象的方案 – 工厂模式

  • 我们可以想到的一种创建对象的方式:工厂模式
    • 工厂模式其实是一种常见的设计模式;
    • 通常我们会有一个工厂方法,通过该工厂方法我们可以产生想要的对象;
// 工厂模式: 工厂函数
function createPerson(name, age, height, address) {
  var p = {}
  p.name = name
  p.age = age
  p.height = height;
  p.address = address

  p.eating = function() {
    console.log(this.name + "在吃东西~")
  }

  p.running = function() {
    console.log(this.name + "在跑步~")
  }

  return p
}

var p1 = createPerson("张三", 18, 1.88, "广州市")
var p2 = createPerson("李四", 20, 1.98, "上海市")
var p3 = createPerson("王五", 30, 1.78, "北京市")

// 工厂模式的缺点(获取不到对象最真实的类型)
console.log(p1, p2, p3)

3.认识构造函数

  • 工厂方法创建对象有一个比较大的问题:我们在打印对象时,对象的类型都是Object类型
    • 但是从某些角度来说,这些对象应该有一个他们共同的类型;
    • 下面我们来看一下另外一种模式:构造函数的方式;
  • 我们先理解什么是构造函数?
    • 构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数;
    • 在其他面向的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法;
    • 但是JavaScript中的构造函数有点不太一样;
  • JavaScript中的构造函数是怎么样的?
    • 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别;
    • 那么如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数;
  • 那么被new调用有什么特殊的呢?

4.new操作符调用的作用

  • 如果一个函数被使用new操作符调用了,那么它会执行如下操作:
      1. 在内存中创建一个新的对象(空对象);
      1. 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;
      1. 构造函数内部的this,会指向创建出来的新对象;
      1. 执行函数的内部代码(函数体代码);
      1. 如果构造函数没有返回非空对象,则返回创建出来的新对象;
    function Person() {}
    
    var p1 = new Person()
    var p2 = new Person()
    
    //Person {}
    console.log(p1)
    

5.创建对象的方案 – 构造函数

我们来通过构造函数实现一下:

// 规范: 构造函数的首字母一般是大写
function Person(name, age, height, address) {
 this.name = name
 this.age = age
 this.height = height
 this.address = address

 this.eating = function() {
   console.log(this.name + "在吃东西~")
 }

 this.running = function() {
   console.log(this.name + "在跑步")
 }
}
  • 这个构造函数可以确保我们的对象是有Person的类型的(实际是constructor的属性,这个我们后续再探讨);
  • 但是构造函数就没有缺点了吗?
    • 构造函数也是有缺点的,它在于我们需要为每个对象的函数去创建一个函数对象实例

6.认识对象的原型

  • JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象。
  • 那么这个对象有什么用呢?
    • 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作;
    • 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它;
    • 如果对象中没有改属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;
  • 那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?
    • 答案是有的,只要是对象都会有这样的一个内置属性;
  • 获取的方式有两种:
    • 方式一:通过对象的 __proto__ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问 题);
    • 方式二:通过 Object.getPrototypeOf 方法可以获取到;

7.函数的原型 prototype

  • 那么我们知道上面的东西对于我们的构造函数创建对象来说有什么用呢?
    • 它的意义是非常重大的,接下来我们继续来探讨;
  • 这里我们又要引入一个新的概念:所有的函数都有一个prototype的属性:
function foo() {

}


// 函数也是一个对象
// console.log(foo.__proto__) // 函数作为对象来说, 它也是有[[prototype]] 隐式原型

// 函数它因为是一个函数, 所以它还会多出来一个显示原型属性: prototype
console.log(foo.ptototype)
  • 你可能会问题,是不是因为函数是一个对象,所以它有prototype的属性呢?
    • 不是的,因为它是一个函数,才有了这个特殊的属性;
    • 而不是它是一个对象,所以有这个特殊的属性;
var obj = {}
console.log(obj.prototype) // obj就没有这个属性

8.再看new操作符

  • 前面讲过new关键字的步骤如下:

    • 1.在内存中创建一个新的对象(空对象);
    • 2.这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;(后面详细讲);
  • 那么也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype:

image.png

function Person() {

}
var p1 = new Person()

// 上面的操作相当于会进行如下的操作:
p = {}
p.__proto__ = Person.prototype

********************************************
function Person() {

}

var p1 = new Person()
var p2 = new Person()
var p3 = new Person()

console.log(p1.__proto__ === p2.__proto__)
console.log(p1.__proto__ === Person.prototype)

9.创建对象的内存表现

image.png

10.赋值为新的对象

image.png

11.prototype添加属性

image.png

12. constructor属性

  • 事实上原型对象上面是有一个属性的:constructor
    • 默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象;
function Person() {

}

console.log(Person.protopyte.constructor) // [Function: Person]
console.log(p1.__proto__.constructor)     // [Function: Person]
console.log(p1.__proto__.constructor.name) //  Person

13.重写原型对象

  • 如果我们需要在原型上添加过多的属性,通常我们会重新整个原型对象:
function Person() {

}

Person.prototype = {
    name: 'mint',
    age: 18,
    eating: function() {
        console.log(this.name + "在干饭~~")
    }
}
  • 前面我们说过, 每创建一个函数, 就会同时创建它的prototype对象, 这个对象也会自动获取constructor属性;
    • 而我们这里相当于给prototype重新赋值了一个对象, 那么这个新对象的constructor属性, 会指向Object构造函数, 而不是Person构造函数了

14.原型对象的constructor

  • 如果希望constructor指向Person,那么可以手动添加:
  • 上面的方式虽然可以, 但是也会造成constructor的[[Enumerable]]特性被设置了true.
    • 默认情况下, 原生的constructor属性是不可枚举的.
    • 如果希望解决这个问题, 就可以使用我们前面介绍的Object.defineProperty()函数了.
function Person() {

}

Person.prototype = {
    name: 'mint',
    age: 18,
    eating: function() {
        console.log(this.name + "在干饭~~")
    }
}

Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
    value: Person
})

15.创建对象 – 构造函数和原型组合

  • 在上一个构造函数的方式创建对象时,有一个弊端:会创建出重复的函数,比如running、eating这些函数
    • 那么有没有办法让所有的对象去共享这些函数呢?
    • 可以,将这些函数放到Person.prototype的对象上即可;
function Person(name, age, height, address) {
  this.name = name
  this.age = age
  this.height = height
  this.address = address
}

Person.prototype.eating = function() {
  console.log(this.name + "在干饭~")
}

Person.prototype.running = function() {
  console.log(this.name + "在跑步~")
}

var p1 = new Person("why", 18, 1.88, "北京市")
var p2 = new Person("kobe", 20, 1.98, "洛杉矶市")

p1.eating()
p2.eating()