上一讲我们学习了Object.defineProperty,这个API是用来定义一个对象的某个属性的值以及相关权限操作,但是只能同时定义一个属性,那有没有一次性能定义多个属性的API呢?答案是有的。那就是Object.defineProperties。
Object.defineProperties(obj, props)
const obj = {
_age: 22 // 私有属性
}
Object.defineProperties(obj, {
name: {
configurable: true,
enumerable: true,
writable: true,
value: 'zengge'
},
age: {
configurable: true,
enumerable: true,
set(value) {
this._age = value
},
get() {
return this._age
}
}
})
除此之外,还有很多关于属性描述符的API,下面我列举一下常见的几个:
Object.getOwnPropertyDescriptor(obj, prop),获取一个对象的某个属性的属性描述符。Object.getOwnPropertyDescriptors(obj),获取一个对象的所有属性的属性描述符。Object.preventExtensions(obj),禁止一个对象添加新的属性。Object.seal(obj),禁止阻止向对象添加新的属性和删除属性,但是可修改或者更新现有属性。Object.freeze(obj),禁止一个对象的所有属性的任何操作。
认识构造函数
构造函数也叫构造器(constructor),在其他编程语言中,构造函数是在类中的一个方法,称为构造方法。在Javascript中的构造函数其实跟普通函数没有任何的区别,但是如果一个普通函数被new关键词调用了,那么这个函数就叫构造函数。
function foo() {
console.log()
}
foo() // 直接调用为普通函数
new foo() // new关键词调用为构造函数
new关键词我们之前在this章节中提到过,下面我们就来详细地聊聊new关键字。
如果一个函数被new关键字调用了,会执行什么操作?
- 在内存在创建一个新的对象
- 这个新对象的内部的
[[prototype]]属性会被赋值为该构造函数的prototype属性 - 构造函数中的
this指向将会指向创建出的新对象 - 开始执行构造函数体
- 若构造函数没有返回任何非空对象,则返回创建出来的新对象
如果我们要批量创建一堆有相同属性的对象,我们可以使用
构造函数:
function Person(age, name, height, size) {
this.age = age
this.name = name
this.height = height
this.size = size
this.running = function() {
console.log(`${this.name}正在跑步`)
}
}
const person1 = new Person(22, '小明', 1.68, 'large')
const person2 = new Person(23, '小红', 1.58, 'large')
const person3 = new Person(18, '小曾', 1.78, 'size')
这样我们就批量创建了具有相同属性的对象,注意构造函数的函数名以大写字母开头,这是约定俗成的写法。但是构造函数是不是批量生成对象的最好解决方案呢?并不是,构造函数会有很多的缺点。
构造函数的缺点
当我们批量创建对象时,running方法其实总是会被重新创建,这样的话如果说某个构造函数中有很多函数方法,那么批量创建很多对象的时候会造成成内存的浪费。既然没有必要这样那有没有其他的解决方案呢?是有的。我们先来理解一下原型(prototype)这个概念。
对象的原型
每个对象中会有一个属性叫[[Prototype]],但是它比较特殊,它可以指向另一个对象。早期的ECMA并没有明确规定如何去查看[[Protootype]],但是为了让开发者能查看这个对象,浏览器厂商给我们提供了一个属性叫__proto__,被称为对象的隐式原型。
函数的原型
刚刚我们说的是对象的原型__proto__,同样函数也有原型,函数的原型是一个prototype的属性,且只有函数有prototype这个属性,跟__proto__有如下关系:
function Foo() {
console.log('foo')
}
const foo = new Foo()
foo.__proto__ === Foo.prototype // true
我们之前说过new关键字在执行的过程中的第二步是将这个空对象的原型指向构造函数本身的原型,所以你知道了为什么foo.__proto__ === Foo.prototype为true了吧。foo就是这个空对象(因为new操作符最终会把空对象返回出去也就是赋值给foo),而Foo就是构造函数。ok回到刚刚的问题,针对构造函数的缺点我们有没有什么其他的解决办法呢?
function Person(age, name, height, size) {
this.age = age
this.name = name
this.height = height
this.size = size
}
Person.prototype.running = function() {
console.log(`${this.name}在跑步`)
}
const person1 = new Person(22, '小明', 1.68, 'large')
const person2 = new Person(23, '小红', 1.58, 'large')
const person3 = new Person(18, '小曾', 1.78, 'size')
person1.running()
person2.running()
person3.running()
这样也就解决了之前的问题,将函数方法添加到构造函数的原型上,使得每个构造函数的实例都可以访问到running函数且不会造成内存浪费。那有同学就要问了,那name,age,height等属性能不能也放到prototype上面呢?不可以。因为如果也像函数方法一样放到原型prototype上,那么上一个对象实例传进来的参数会被下一个覆盖掉,从而最终的值是最后一个对象实例的参数的值。