09_对象补充-原型-创建对象

对象方法补充

就是一些 API 接口,这里就不展开了

  • 获取对象的属性描述符

    • getOwnPropertyDescriptor
    • getOwnPropertyDescriptors
  • 禁止对象扩展新属性

    • preventExtensions
    • 给一个对象添加新的属性会失败(在严格模式下会报错)
  • 密封对象,不允许配置和删除属性

    • seal
    • 实际是调用 preventExtensions
    • 并且将现有属性的 configurable: false
  • 冻结对象,不允许修改现有属性

    • freeze
    • 实际上是调用 seal
    • 并且将现有属性的 writable: false

构造函数创建对象的方式

在上一节中我们有对象字面量和工厂模式两种方法,但是都有缺陷: 不方便 和 没有对应类型

思考: 在工厂模式中,我们将所有的变量都定义在工厂函数中,那么假设我们创建出来的对象具有一些相同的方法的时候,采用工厂模式会造成大量的内存浪费并且没有对应的类型,我们先解决一个问题。没有对应的类型,即我们的构造函数模式

认识构造函数

  • 工厂方法创建对象有一个比较大的问题:我们在打印对象时,对象的类型都是 Object 类型

    • 但是从某些角度来说,这些对象应该有一个它们共同的类型
    • 下面我们来看一下另外一种模式:构造函数的方式
  • 我们先理解什么是构造函数?

    • 构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数
    • 在其它面向对象的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法
    • 但是 JavaScript 中的构造函数有点不太一样
  • JavaScript 中的构造函数是怎么样的?

    • 构造函数也是一个普通的函数,从表现形式来说,和其它普通的函数没有任何区别
    • 那么如果这么一个普通的函数被使用 new 操作符来调用了,那么这个函数就称之为是一个构造函数
  • 那么被 new 调用有什么特殊的呢?

new 操作符调用的作用

当一个函数被 new 操作符调用时,它就被称为构造函数

如果一个函数被使用 new 操作符调用了,那么他会执行如下操作:

  1. 在内存中创建一个新的对象(空对象)
  2. 这个对象内部的 [[prototype]] 属性会被赋值为该构造函数的 prototype 属性
  3. 构造函数内部的 this,会指向创建出来的新对象
  4. 执行函数的内部代码
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象

使用构造函数模式创建对象

这里我们使用了构造函数去创建我们的对象,解决了工厂模式没有类型的缺陷,但是它就完美了吗? 仔细看这里的 eating 和 running 函数,试想我们每次通过 new 关键词创建出一个对象后,这两个函数也会被同时开辟出内存空间,但是真的有必要吗,实际是没有必要的。

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

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

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

var p1 = new Person("张三", 18);
var p2 = new Person("李四", 28);

构造函数加原型的创建对象的方式

认识对象的原型

注意:下面用[[]]标识的 prototype 指的是在 MDN 中定义的概念,但是浏览器有自己的实现通过不同的方法获取,它的属性不是叫[[prototype]] 一般我们把对象中的这个原型称之为隐式原型

  • 首先在 JavaScript 中每一个对象都有一个特殊的内置属性[[prototype]],这个特殊的对象可以指向另外一个对象

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

    • 当我们通过引用对象的属性 key 来获取一个 value 时,他会触发[[Get]]的操作;
    • 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它;
    • 如果对象中没有改属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性
  • 那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?

    • 答案是有的,只要是对象都会有这样的一个内置属性;
  • 获取的方式有两种:

    • 方式一:通过对象的__proto__属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题)
    • 方式二:通过 Object.getPrototypeOf 方法可以获取到(ECMAScript 后续添加的方法)

函数的原型

  • 上面讲了所有的对象都有一个[[prototype]]属性,同样的,在函数中也存在类似的一个叫 prototype 的属性,称之为函数的原型,我们也将函数的原型称之为显示原型

  • 要注意因为它是函数,才有这个特殊的属性,而不是因为函数是一个对象,所以才有这个属性,注意区分

再看 new 操作符

我们前面讲过 new 关键词的步骤

  • 在内存中创建一个对象(空对象)
  • 在这个对象内部的[[prototype]]属性会被赋值为该构造函数的 prototype 属性

那么也就意味着我们通过 Person 构造函数创建出来的所有对象的[[prototype]]属性都指向 Person.prototype

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);

创建对象的内存表现

创建对象的内存表现

如上图:

函数有一个显示原型属性 prototype,它指向自己的原型对象,同时在使用 new 操作符创建出来的 p1 和 p2 对象中存在隐式原型属性__proto__,new 操作符的第二步就是将对象中的隐式原型属性赋值为 Person 的显示原型属性,即 Person 中的原型对象

  • 我们看到 Person 的原型对象中有一个属性 constructor
    • 默认情况下原型上都会添加一个属性叫做 constructor,这个 constructor 指向当前的函数对象(重新指回去)

重写原型对象

如果我们需要在原型上添加过多的属性,通常我们会重写整个原型对象

function Person() {}

Person.prototype = {
  name: "beanbag",
  age: 18,
  eating: function () {
    console.log(this.name + "在吃饭");
  },
};

前面我们说过,每创建一个函数,就会同时创建它的 prototype 属性,这个对象也会自动获取 constructor 属性
而我们这里相当于给 prototype 重新赋值了一个对象,那么这个新对象的 constructor 属性,会指向 Object 构造函数,而不是 Person 构造函数了

原型对象的 constructor

如果希望 constructor 指向 Person,那么可以手动添加:

Person.prototype = {
  constructor: Person,
  name: "beanbag",
  age: 18,
  eating: function () {
    console.log(this.name + "在吃饭");
  },
};

上面的方式虽然可以,但是也会造成 constructor 的[[Enumerable]]特性被设置了 true

  • 默认情况下,原生的 constructor 属性是不可枚举的
  • 如果希望解决这个问题,就可以使用我们前面介绍的 Object.defineProperty()函数了
Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person,
});

创建对象

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

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

var p1 = new Person("beanbag",18.1.88,"深圳市");

总结一下

  1. 对象的隐式原型属性等于其构造函数的显示原型属性
  2. 构造函数的显示原型属性指向其原型对象的 constructor 属性
  3. 构造函数的原型对象的 constructor 属性指向构造函数本身(循环引用)
Last Updated:
Contributors: hqchqc