面向对象是现实的抽象方式
JavaScript 的面向对象
创建对象的两种方式
- new 构造函数
- 字面量 ```javascript let obj = new Object()
// 动态添加属性和方法 obj.name = ‘zs’ obj.age = 18 obj.foo = function(){ console.log(this.name + ‘在跳舞’) }
obj.foo()
```javascript
let obj = {
name: 'zs',
age: 18,
foo: function() {
console.log(this.name + '在跳舞');
}
}
obj.foo()
对对象属性的操作
对对象属性增删查改都是可以的,没有任何限制。
let obj = {
name: 'zs',
age: 18,
dance: function() {
console.log(this.name + '在跳舞');
}
}
// 增
obj.sing = function() {
console.log(this.name + '在唱歌')
}
// 删
delete obj.age;
// 查
console.log(obj.name);
// 改
obj.name = 'ls';
console.log(obj)
// { name: 'ls', dance: [Function: dance], sing: [Function (anonymous)] }
其实属性也有一些描述符,用来描述这个属性。就像属性的属性,叫做属性描述符。属性描述符是一个对象。
怎么修改属性的属性呢?Object.defineProperty()
方法。
Object.defineProperty( )
Object.defineProperty()
方法能直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
语法
Object.defineProperty(obj, prop, descriptor)
参数
- obj
要在其上定义属性的对象。
- prop
要定义或修改的属性的名称。
- descriptor
将被定义或修改的属性描述符。
返回值
被传递给函数的对象。
let obj = {
name: 'zs',
age: 18,
dance: function() {
console.log(this.name + '在跳舞');
}
}
Object.defineProperty(obj, 'age', {
// 编辑属性描述符的属性值
})
属性描述符分类
- 数据属性(Data Properties)描述符(Descriptor);
- 存取属性(Accessor访问器 Properties)描述符(Descriptor);
|
| configurable | enumerable | value | writable | get | set | | —- | —- | —- | —- | —- | —- | —- | | 数据描述符 | 有 | 有 | 有 | 有 | 无 | 无 | | 存取描述符 | 有 | 有 | 无 | 无 | 有 | 有 |
总结一下就是:**述同一个属性,存在value、writable 则该属性描述符为描述数据的,反之则为描述存取的。
数据数据描述符有四个特性:
Configurable
表示属性是否能被 delete 删除,以及是否能被属性描述符重新描述,也就是属性描述能否被修改。
默认值:
- 直接在对象上定义的属性,这个属性的 [[Configurable]] 为 true;
- 通过
Object.defineProperty()
定义的属性,属性描述符中的 [[Configurable]] 默认为 false;
let obj = {
name: 'zs',
age: 18,
dance: function() {
console.log(this.name + '在跳舞');
}
}
Object.defineProperty(obj, 'sing', {
enumerable: true // 没有手动定义 configurable,取了它的默认值
})
console.log(obj); // { name: 'zs', age: 18, dance: [Function: dance], sing: undefined }
delete obj.age // 删除直接定义在对象中的属性 age
delete obj.sing // 删除刚刚通过定义原型的方法定义的属性 sing
console.log(obj); // { name: 'zs', dance: [Function: dance], sing: undefined }
// age 被删除,sing 删除没被删除,证实了不同情况下默认值为 true 和 false
Object.defineProperty(obj, 'sing', {
configurable: true // 再次定义属性 sing
})
console.log(obj) // node: Cannot redefine property: sing
Enumerable
表示属性是否可以通过 for-in 或者 Object.keys() 返回该属性,也就是能否被枚举
默认值:
- 直接在对象上定义的属性,这个属性的[[Enumerable]]为 true;
- 通过
Object.defineProperty()
定义的属性,这个属性的[[Enumerable]]默认为 false; ```javascript let obj = { name: ‘zs’, age: 18, dance: function() { console.log(this.name + ‘在跳舞’); } }
// name 枚举默认值为 true,能被枚举 console.log(obj); // { name: ‘zs’, age: 18, dance: [Function: dance] } for (const key in obj) { console.log(key); // name、age、dance } console.log(Object.keys(obj)); // [ ‘name’, ‘age’, ‘dance’ ]
// 新增属性 sing 枚举默认为 false,无法枚举 Object.defineProperty(obj, ‘sing’, { configurable: true }) console.log(Object.keys(obj)); // [ ‘name’, ‘age’, ‘dance’ ]
Object.defineProperty(obj, ‘sing’, { enumerable: true // 修改为true }) // 成功枚举 sing console.log(Object.keys(obj)); // [ ‘name’, ‘age’, ‘dance’, ‘sing’ ]
<a name="Ikq1Y"></a>
### Writable
表示是否可以修改属性的值;
默认值:
- 直接在对象上定义的属性,这个属性的[[Writable]]为 true;
- 通过`Object.defineProperty()`定义的属性,这个属性的[[Writable]]默认为 false;
```javascript
let obj = {
name: 'zs',
dance: function() {
console.log(this.name + '在跳舞');
}
}
// writable 默认为 true,可直接修改属性值
obj.name = 'ls'
console.log(obj.name); // ls
// 修改属性描述
Object.defineProperty(obj, 'name', {
writable: false
})
obj.name = '666'
console.log(obj.name); // ls,还好ls ,修改属性值失败
value
属性的 value 值,读取属性时会返回该值,也就是属性值,修改属性时,会对其进行修改;
默认情况下这个值是 undefined;
存取属性描述符
它也有四个特性,configurable
、enumerable
和数据属性描述符一样。特有的是set
、get
set 和 get 是两个方法,是在存取属性时会自动调用的方法,默认为undefined。就像 Java 中的 setter 和 getter。
有两大作用:
- 隐藏了某一个私有属性不希望直接被外界使用和赋值
- 可以让我们截获某一个属性它访问和设置值的过程,并做一些处理。就像代理模式一样,增强了功能。 ```javascript let obj = { name: ‘zs’, _secret: ‘hhh’, // 私有属性不想暴露出去(一般私有前面会有下划线) dance: function() { console.log(this.name + ‘在跳舞’); } }
// 向外暴露 secret 来存取_secret 的值,这样就隐藏了 _secret 属性名 Object.defineProperty(obj, ‘secret’, { configurable: true, enumerable: true, set: function(value) { log.apply(‘set’) // 在存值过程中做些处理 this._secret = value }, get: function() { log.apply(‘get’) // 在取值过程中做些处理 return this._secret } })
function log() { console.log(this + ‘被调用了’) }
// 取值过程,自动调用了 get() console.log(obj.secret); // get被调用了, hhh
// 存值过程,自动调用了 set() obj.secret = 123 // set被调用了 console.log(obj.secret); // get被调用了, 123
**通过存取属性描述符,截获存取属性过程,其实就是 vue2 的响应式原理。**
setter 和 getter 还有另一种写法,这种写法会**默认 configurable 和 enumerable 为 true**
```javascript
let obj = {
name: 'zs',
_age: 18,
// 直接写setter getter
set age(value) {
this._age = value
},
get age() {
return this._age
}
}
console.log(obj); // { name: 'zs', _age: 18, age: [Getter/Setter] }
同时定义多个属性
Object.defineProperty()
方法有个复数形式的方法Object.defineProperties()
,它可以一次性定义多个属性。
let obj = {
name: 'zs',
dance: function () {
console.log(this.name + ' is dancing')
}
}
Object.defineProperties(obj, {
// 属性名
sing: {
// 属性描述
configurable: true,
enumerable: true,
writable: true,
value: function () {
console.log(this.name + ' is singing')
}
}
})
console.log(obj) // { name: 'zs', dance: [Function: dance], sing: [Function: value] }
obj.sing() // zs is singing
对象方法补充
获取对象的属性描述符:
getOwnPropertyDescriptor()
getOwnPropertyDescriptors()
```javascript let obj = { name: ‘zs’, dance: function () { console.log(this.name + ‘ is dancing’) } }
// 打印 name 的属性描述符 console.log(Object.getOwnPropertyDescriptor(obj, ‘name’)); // { value: ‘zs’, writable: true, enumerable: true, configurable: true }
// 打印对象所有属性的属性描述符 console.log(Object.getOwnPropertyDescriptors(obj))
// { // name: { value: ‘zs’, writable: true, enumerable: true, configurable: true }, // dance: { // value: [Function: dance], // writable: true, // enumerable: true, // configurable: true // } // }
对对象做一些限制:
1. 禁止对象扩展新属性: `preventExtensions()`给一个对象添加新的属性会失败(在严格模式下会报错);
```javascript
let obj = {
name: 'zs',
dance: function () {
console.log(this.name + ' is dancing')
}
}
// 阻止扩展属性
Object.preventExtensions(obj)
Object.defineProperty(obj, 'age', { // node:Cannot define property age, object is not extensible
configurable: true,
enumerable: true
})
obj.age = 18;
console.log(obj); // { name: 'zs', dance: [Function: dance] }
- 密封对象,不允许配置和删除属性:
seal()
- 实际是调用 preventExtensions 并且将现有属性的设为
configurable:false
- 所以也可以一个一个属性遍历然后修改它们的 configurable,但 seal( ) 封装了这个过程,更方便了 ```javascript let obj = { name: ‘zs’, dance: function () { console.log(this.name + ‘ is dancing’) } }
console.log(Object.getOwnPropertyDescriptors(obj)); // { // name: { value: ‘zs’, writable: true, enumerable: true, configurable: true }, // dance: { // value: [Function: dance], // writable: true, // enumerable: true, // configurable: true // } // }
// 封闭对象 Object.seal(obj)
obj.age = 18; // 动态添加属性
console.log(Object.getOwnPropertyDescriptors(obj));
// 所有属性的 configurable 都变成了 false,并且属性中没有 age 属性
// { // name: { // value: ‘zs’, // writable: true, // enumerable: true, // configurable: false // }, // dance: { // value: [Function: dance], // writable: true, // enumerable: true, // configurable: false // } // }
3. 冻结对象,不允许修改现有属性: `freeze()`
- 实际上是调用 seal,并且将现有属性的`writable: false`
- 因为是在其他限制基础上加码,**这几乎是最严格的限制了**。
```javascript
let obj = {
name: 'zs',
dance: function () {
console.log(this.name + ' is dancing')
}
}
// 冻结对象
Object.freeze(obj)
obj.name = 'ls'; // 修改属性
console.log(Object.getOwnPropertyDescriptors(obj));
// 在 seal 基础上 ,所有属性的 writable 变成了 false
// {
// name: {
// value: 'zs',
// writable: false,
// enumerable: true,
// configurable: false
// },
// dance: {
// value: [Function: dance],
// writable: false,
// enumerable: true,
// configurable: false
// }
// }
创建多个对象的方式
我们都知道有两种创建对象的方法,字面量 和 new 关键字。
字面量创建对象,需要不停的 let 和不停地给属性赋值。重复写了大量代码,很麻烦。
let p1 = {name: 'zs', age: 18}
let p2 = {name: 'ls', age: 19}
为了改进这一点可以使用 工厂设计模式,批量创建对象。但是仍然存在一个问题就是,创建出来的对象是没有类型的,全是 Object。
function factory(name, age) {
// create an empty object
let p = {}
// 动态赋值
p.name = name
p.age = age
// 将对象返回
return p
}
let p1 = factory('zs', 18)
let p2 = factory('ls', 28)
console.log(p1) // Object { name: 'zs', age: 18 }
console.log(p2) // Object { name: 'ls', age: 28 }
所以批量创建对象使用 字面量的方式就不是很合适。这就得使用 new 构造函数的方式了。
构造函数
构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数;在其他面向的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法;但是JavaScript中的构造函数有点不太一样;
- JavaScript 中的构造函数:
构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别;那么如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数;为了方便区分这是一个构造函数,通常会大写函数名首字母。
new 操作符调用函数的执行过程
- 在内存中创建一个新的对象(空对象);
- 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;
- 构造函数内部的this,会指向创建出来的新对象;
- 执行函数的内部代码(函数体代码);
- 如果构造函数没有返回非空对象,则返回创建出来的新对象;
构造函数其实就是普通函数,和上面的工厂函数没啥区别。但是我们用 new 关键字一调用,就让这个普通函数相当于省略了工厂函数定义空对象和返回对象的步骤,并且最重要的,克服了工厂函数的缺陷。
function Person(name, age) {
this.name = name
this.age = age
this.dance = function() {
console.log(this.name + " It's time to dance");
}
}
let p1 = new Person('zs', 18)
let p2 = new Person('ls', 28)
console.log(p1); // Person { name: 'zs', age: 18, dance: [Function (anonymous)] }
console.log(p2); // Person { name: 'ls', age: 28, dance: [Function (anonymous)] }
构造函数的缺点
构造函数也并不是完美的方案,它也有缺点。当对象中定义方法时,每次通过构造函数执行生成一个对象,堆内存中就会生成成员方法的函数对象。一个对象中定义了两个方法,生成100个对象,就会有200个函数对象。而这些函数的功能都是一模一样的,这样就浪费了内存。
function Person(name, age) {
this.name = name
this.age = age
this.dance = function() { // 会重复生成函数对象
console.log(this.name + " It's time to dance");
}
}
let p1 = new Person()
let p2 = new Person()
// p1 p2 两个对象各自新建了一个函数对象
console.log(p1.dance === p2.dance); // false
解决办法就是原型。