七. 面向对象
7.1. 属性描述符
如果属性是直接定义在对象内部,或者直接添加到对象内部,这时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过delete删除的?这个属性是否在for-in遍历的时候被遍历出来呢?
var obj = {
name:"why",
age:18
}
delete obj.name
for(var key in obj){
console.log(key);
}
如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符(Object.defineProperty)。
-
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor) -
可接收三个参数:
- obj要定义属性的对象;
- prop要
定义或修改的属性的名称或 Symbol; - descriptor要定义或修改的属性描述符;
-
返回值:
- 被传递给函数的对象。
7.2. 属性的类型(数据/访问器属性)
7.2.1 数据属性
数据数据描述符有如下四个特性:
-
[[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true;
- 当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false;
-
[[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true;
- 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false;
-
[[Writable]]:表示是否可以修改属性的值;
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true;
- 当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false;
-
[[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改;
- 当我们直接在一个对象上定义某个属性时,这个属性的[[value]]特性会被设置为指定的值;
- 默认情况下这个值是undefined;
7.2.2. 存取属性(访问器属性)
访问器属性不包含数据值(没有[[Writable]]和[[value]])。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。
-
[[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;
- 和数据属性描述符是一致的;
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true;
- 当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false;
-
[[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;
- 和数据属性描述符是一致的;
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true;
- 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false;
-
[[get]]:获取属性时会执行的函数。默认为undefined
-
[[set]]:设置属性时会执行的函数。默认为undefined
7.3. 定义多个属性
Object.defineProperties() 方法直接在一个对象上定义 多个 新的属性或修改现有属性,并且返回该对象。
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
}
});
这段代码在 book 对象上定义了两个数据属性 year_和 edition,还有一个访问器属性 year。最终的对象跟上一节示例中的一样。唯一的区别是所有属性都是同时定义的,并且数据属性的configurable、enumerable 和 writable 特性值都是 false。
7.4. 对象方法补充
-
获取对象的属性描述符
-
Object.getOwnPropertyDescriptor(对象,属性名)
- 返回值是一个对象
-
Object.getOwnPropertyDescriptors(对象)
- ES7新增方法,返回值是一个对象
let obj = { name: "why", age: 18 } let result = Object.getOwnPropertyDescriptor(obj, "name") console.log(result); console.log(result.writable); let results= Object.getOwnPropertyDescriptors(obj) console.log(results); -
-
禁止对象继续添加新属性
-
Object.preventExtensions(对象)
- 给一个对象添加新的属性会失败(在严格模式下会报错);
-
-
禁止对象配置/删除里面的属性
-
Object.seal(对象)
- 将现有属性的[[Configurable]] 设为了false
- 实际是调用preventExtensions,并且将现有属性的configurable:false
-
-
冻结属性,禁止属性修改
-
Object.seal(对象)
- 实际上是调用seal,并且将现有属性的writable: false
-
7.5. 创建多个对象的方法
创建对象通过会用new Object() 和 字面量方式,都存在一个很大的弊端,创建同样的对象时,需要编写重复的代码
7.5.1. 工厂模式
工厂模式是一种常见的设计模式,缺点是获取不到对象的真实类型,都是Object类型
function createPerson(name, age, height, addresss) {
let p = new Object()
p.name = name
p.age = age
p.height = height
p.addresss = addresss
return p
}
let p1 = createPerson("张三", 18, 180, "北京市")
let p2 = createPerson("李四", 19, 181, "成都市")
let p3 = createPerson("王五", 21, 182, "上海市")
7.5.2. 构造函数
什么是构造函数
- 构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数,
- 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别;
- 如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数
使用new关键字来调用函数时,会执行如下的操作
-
在内存中创建一个新对象(空对象)。
-
这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。222 第 8 章 对象、类与面向对象编程
-
构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
-
执行构造函数内部的代码(给新对象添加属性)。
-
如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
// 下面是模拟代码 function foo() { // 1.创建一个空对象 var moni = {} // 2.将函数的prototype赋值给空对象的__proto__ moni.__proto__ = foo.prototype // 3.this指向新对象 this = moni // 4.执行函数内部代码 // 5.返回moni return moni }
构造函数的缺点
-
我们需要为每个对象的函数去创建一个函数对象实例;
-
如果构造函数里没有函数则 没有这个缺点
function Person() { this.name = "why" this.eating = function () { console.log("再吃东西"); } this.runing = function () { console.log("在跑步"); } } let p1 = new Person("张三") let p2 =new Person("李四") // 每次都会执行new Person()时都会给每个对象的eating和runing属性创建函数对象,但每个对象的eating()和runing()都是一样的,每次都在重复的创建eating和runing函数对象 console.log(p1.eating === p2.eating); // false console.log(p1.runing === p2.runing); // false
7.5.3. 补充:js模拟实现new函数
function objectFactory() {
// 1.创建一个空对象
var obj = new Object()
// 删除并返回arguments的第一个参数 (会改变原数组)
Constructor = [].shift.call(arguments);
// 2.将函数的prototype赋值给空对象的__proto__
obj.__proto__ = Constructor.prototype;
// 3&&4 把Person的this绑定为obj,并执行Person,实际是给obj添加name,age属性
var ret = Constructor.apply(obj, arguments);
// ret || obj 这里这么写考虑了构造函数显示返回 null或对象 的情况
// 返回创建的对象
return typeof ret === 'object' ? ret || obj : obj;
};
function person(name, age) {
this.name = name
this.age = age
// return null
// return []
// return {adress:"成都"}
}
let p = objectFactory(person, '布兰', 12)
console.log(p) // { name: '布兰', age: 12 }
[].shift.call(arguments) 等同 Array.prototype.shift.call(arguments)
shift内部实现是使用的this代表调用对象。那么当[].shift.call() 传入 arguments对象的时候,通过 call函数改变原来 shift方法的this指向, 使其指向arguments,并对arguments进行复制操作,而后返回第一个参数。至此便是完成了arguments类数组转为数组的目的!
7.5.4. 原型和构造函数结合
将这些函数放到Person.prototype的对象上,让所有的对象去共享这些函数,
function Person() {
this.name = "why"
}
Person.prototype.eating=function(){
console.log(this.name + "在吃东西");
}
Person.prototype.runing=function(){
console.log(this.name + "在跑步");
}
let p1 = new Person("张三")
let p2 =new Person("李四")
问:name可以写在原型吗? 答:不可以,因为每次创建对象的name都会覆盖之前原型的name,每次创建的Person对象都是空的,当执行p1.name时,在自己的对象上都找不到,然后去原型找,结果每个对象的name都是”李四“
问:原型里的this执行会有问题吗? 答:不会,this指向和函数放的位置没有关系,运行时才绑定,比如p1.eating(),this指向的就是p1对象
7.6补充:关于枚举Enumerable
Enumerable为false表示不可枚举,node打印是看不到该属性的,但浏览器可以,这是浏览器方便我们查看对象的属性,明显可以看到,age的颜色是暗色的,这也是一个区分,表示age和name的枚举属性是不同的