09_对象补充-原型-创建对象
对象方法补充
就是一些 API 接口,这里就不展开了
获取对象的属性描述符
- getOwnPropertyDescriptor
- getOwnPropertyDescriptors
禁止对象扩展新属性
- preventExtensions
- 给一个对象添加新的属性会失败(在严格模式下会报错)
密封对象,不允许配置和删除属性
- seal
- 实际是调用 preventExtensions
- 并且将现有属性的 configurable: false
冻结对象,不允许修改现有属性
- freeze
- 实际上是调用 seal
- 并且将现有属性的 writable: false
构造函数创建对象的方式
在上一节中我们有对象字面量和工厂模式两种方法,但是都有缺陷: 不方便 和 没有对应类型
思考: 在工厂模式中,我们将所有的变量都定义在工厂函数中,那么假设我们创建出来的对象具有一些相同的方法的时候,采用工厂模式会造成大量的内存浪费并且没有对应的类型,我们先解决一个问题。没有对应的类型,即我们的构造函数模式
认识构造函数
工厂方法创建对象有一个比较大的问题:我们在打印对象时,对象的类型都是 Object 类型
- 但是从某些角度来说,这些对象应该有一个它们共同的类型
- 下面我们来看一下另外一种模式:构造函数的方式
我们先理解什么是构造函数?
- 构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数
- 在其它面向对象的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法
- 但是 JavaScript 中的构造函数有点不太一样
JavaScript 中的构造函数是怎么样的?
- 构造函数也是一个普通的函数,从表现形式来说,和其它普通的函数没有任何区别
- 那么如果这么一个普通的函数被使用 new 操作符来调用了,那么这个函数就称之为是一个构造函数
那么被 new 调用有什么特殊的呢?
new 操作符调用的作用
当一个函数被 new 操作符调用时,它就被称为构造函数
如果一个函数被使用 new 操作符调用了,那么他会执行如下操作:
- 在内存中创建一个新的对象(空对象)
- 这个对象内部的
[[prototype]]
属性会被赋值为该构造函数的 prototype 属性 - 构造函数内部的 this,会指向创建出来的新对象
- 执行函数的内部代码
- 如果构造函数没有返回非空对象,则返回创建出来的新对象
使用构造函数模式创建对象
这里我们使用了构造函数去创建我们的对象,解决了工厂模式没有类型的缺陷,但是它就完美了吗? 仔细看这里的 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]]
内置属性指向的对象上的属性
- 当我们通过引用对象的属性 key 来获取一个 value 时,他会触发
那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?
- 答案是有的,只要是对象都会有这样的一个内置属性;
获取的方式有两种:
- 方式一:通过对象的
__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,"深圳市");
总结一下
- 对象的隐式原型属性等于其构造函数的显示原型属性
- 构造函数的显示原型属性指向其原型对象的 constructor 属性
- 构造函数的原型对象的 constructor 属性指向构造函数本身(循环引用)