js并不是原生支持面向对象的,面向对象在js中用离散、繁复的语法表示出来。
我觉得如果把单个对象从整个对象模型中拿出来讨论,可以让人更容易接触js中对象的本质,即一种有行为的数据结构。
单个对象
对象在宏观上讲,就是将一组数据和对其进行操作的函数封装在一起的“一坨”东西。
对象字面量
对象字面量是封闭在花括号对({})中的一个对象的零个或多个”属性名-值”对的(元素)列表。
属性名和值之间用:
连接,元素之间用,
分隔
下面通过定义一个对象字面量并赋值给变量,创建了一个student
对象:
var student = {
name:'Jack',
id:'12306',
grade:99
};
对象是什么
我们这里先不谈面向对象,因为面向对象是很多对象的组合设计,这里我们只讨论单个对象。
如果按数据结构来看的话,
js的对象就是键值对的合集。
有些像是c++里的struct
与std::map
的混合体。
不过struct
里的成员不可改,std::map
是个同质容器(元素都是同类型的)。
而如果从含义上来看的话,js和c++里的类的对象都是一样的,即表示为一个有自己行为的个体。
属性与方法
属性就是附加到对象上的变量。
在上一节的例子里,student
对象分别有3个属性:name
id
grade
提到变量,我们之前说过:
- 在js里只有1种非原始类型,即对象
- 对象可以赋值给变量
- 函数也是对象
自然,函数也可以赋值给变量。
所以,属性也可以是函数。
函数类型的属性也称为方法。
在c++中,术语有些不同:
成员变量对应js的不是函数类型的属性,成员函数对应js的方法。
setter与getter
c++里,一个成员变量要改变取值和赋值的行为,可以通过重载运算符的方式达到。
js中没有重载运算符,但提供了一套专用的语法,set
和get
。
当尝试设置属性时,set语法将对象属性绑定到要调用的函数。
get语法将对象属性绑定到查询该属性时将被调用的函数。
set的语法如下,prop是属性的名称,val是传入的参数名称:
set prop(val){ ... }
get的语法如下,
get prop(){ ... }
含setter和getter的例子:
var student = {
set grade(number){
this.pass = number>=60? true:false
},
get isPass(){
return this.pass
}
}
student.isPass //=>undefined
student.grade = 59
student.isPass //=>false
student.grade = 61
student.isPass //=>true
可以看出setter和gettter具有惰性行为,使用前不会计算属性的值。
合理利用可以有效减少内存占用和提高运行效率。
属性
访问属性
在js中有2种方法访问属性:点符号、方括号。
- 点符号
student.name //=>'Jack'
student.id //=>'12306'
student.grade //=>99
- 方括号
student['name'] //=>'Jack'
student['id'] //=>'12306'
student['grade'] //=>99
如果访问不存在的属性,会返回undefined
:
student.mother //=>undefined
或
student['mother'] //=>undefined
修改、添加属性
可以直接给已存在属性赋值,以更改它:
student.grade = 61
或
student['grade'] = 61
给不存在的属性赋值,可以添加属性:
student.father = 'Tom'
或
student['father'] = 'Tom'
注意js里修改和添加的语法并没有分开,需要自己判断属性的存在性。
c++中std::map
也有这种语法含糊的情况。
删除属性
js里用delete
操作符可以删除属性,如果删除成功或属性不存在都会返回true,删除失败返回false:
delete student.grade //=>true
student.grade //=>undefined
试图删除变量,或属性的可配置性为否都会删除失败。
c++是静态类型,成员变量和成员函数都是一经定义不可更改的。
c++里的delete
操作符意义也完全不一样,指的是释放对象。
属性存在性
不存在的属性值是undefined
,属性默认值也是undefined
,还可以手动将某个属性值赋为undefined
,鉴定一个属性的存在性需要别的方法。
下面是一个表格,用来鉴定属性的存在性:
方法 | 访问不可枚举 | 访问原型链 | 返回值 |
---|---|---|---|
obj.hasOwnProperty(prop) | 是 | 否 | Boolean |
Object.getOwnPropertyNames(obj) | 是 | 否 | 属性名数组 |
Object.keys(obj) | 否 | 否 | 属性名数组 |
for…in | 否 | 是 | 迭代属性名 |
属性描述符
属性描述符一个描述属性的特性的对象。
属性描述符只能是2种形式之一:数据描述符或存取描述符。
因为属性描述符用来描述属性的特性,属性描述符作为一个对象又有自己的属性;这里有3个“属性”重名,下面将属性描述符的“属性”称为“键”。
两种描述符共有的键:
- configurable
:可配置性,表示能否修改该属性描述符,能否通过delete删除属性。
- enumerable
:可枚举性,表示能否通过for-in或Object.keys访问该属性。
数据描述符的键:
- value
:数据,表示属性的值;默认值为undefined。
- writable
:可写性,表示能否通过赋值运算符修改值。
存取描述符的键:
- get
:在读取值时调用的函数;默认值为undefined。
- set
:在写入值时调用的函数;默认值为undefined。
从上面描述符对象的键可以看出,一个属性如果有setter或者getter,就不能用赋值运算符直接控制值;返之亦然。
这是数据和存取之间的差别。
没有指明后面4个键的属性描述符将被认为是数据描述符。
这是它们的键表:
形式 | 可配置性 | 可枚举性 | 值 | 可写性 | get | set |
---|---|---|---|---|---|---|
数据描述符 | 有 | 有 | 有 | 有 | 无 | 无 |
存取描述符 | 有 | 有 | 无 | 无 | 有 | 有 |
查询属性描述符
Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
obj为对象,prop为属性名称,如果查询到返回属性描述符,否则返回undefined:
Object.getOwnPropertyDescriptor(obj, prop)
Object.getOwnPropertyDescriptors() 方法用来获取一个对象的所有自身属性的描述符。
返回一个容纳所有自身属性描述符的对象:
Object.getOwnPropertyDescriptors(obj)
定义属性描述符
使用点运算符、中括号可以添加属性的时候,不能自定义属性的描述符。
Object.defineProperty()
方法可以自定义属性的描述符。
- 字面量、点运算符或中括号定义的属性
下面做个实验:
var obj = {
a : 1,
set b(x){},
get c(){}
}
obj.d = 2
Object.getOwnPropertyDescriptors(obj)
上述代码最后一句的返回值如下:
a : {value: 1, writable: true, enumerable: true, configurable: true}
b : {get: undefined, set: ƒ, enumerable: true, configurable: true}
c : {get: ƒ, set: undefined, enumerable: true, configurable: true}
d : {value: 2, writable: true, enumerable: true, configurable: true}
__proto__ : Object
不管最后一行,可以看到前4行都是属性描述符。
我们可以看出,字面量、点符号、中括号定义的属性,其描述符如果是Boolean类型的默认值都是true
,其它的为undefined
。
这种方法无法修改可写性、枚举性、可配置性。
Object.defineProperty()
方法显式定义描述符
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
descriptor是描述符对象:
Object.defineProperty(obj, prop, descriptor)
下面实验一下:
var obj = {}
Object.defineProperty(obj, 'a', {}) //=>空的描述符对象
Object.getOwnPropertyDescriptor(obj,'a')
//=>{value: undefined, writable: false, enumerable: false, configurable: false}
从最后一行的返回值可以看出,用这种方法定义的属性其描述符默认值是不同的,Boolean类型的值全对false。
而且我们并没有选择任何数据类型或存取类型的描述符键,默认是数据描述符。
Object.defineProperties() 方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。
这个方法相比于Object.defineProperty()
可以一次性定义一组属性描述符,
props
是属性名和属性描述符的键值对对象:
Object.defineProperties(obj, props)
MDN网站上的例子:
var obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
// etc. etc.
});
顶层对象
前文提到,作用域分为全局作用域和局部作用域;函数内的是局部作用域,不在函数内的是全局作用域。
变量也按同样的规则有全局变量和局部变量。
在全局作用域内用this
可以访问到顶层对象;在浏览器环境下,这个顶层对象的名字是window
。
ES5中,顶层对象相比普通对象有个特别之处:全局变量会自动成为顶层对象的属性。
这被认为是js的设计失误,
在ES6中,let
声明的全局变量并不会自动成为顶层对象的属性。
ES5中全局变量有2种声明方式,在全局作用域里用var
声明,或者在任何地方不用var
声明变量。
如下面代码:
var a = 1
b = 2
;(function (){
c = 3
})()
console.log(window.a) // 打印1
console.log(window.b) // 打印2
console.log(window.c) // 打印3
// 这段代码放在全局作用域里,所以这时this就是顶层对象window
// 下面执行的结果和上面一样
console.log(this.a)
console.log(this.b)
console.log(this.c)
但是通过var
定义的顶层对象的属性的描述符会不一样:
Object.getOwnPropertyDescriptor(window,'a')
//=>{value: 1, writable: true, enumerable: true, configurable: false}
Object.getOwnPropertyDescriptor(window,'b')
//=>{value: 2, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(window,'c')
//=>{value: 3, writable: true, enumerable: true, configurable: true}
从上面代码的返回值可以看出,var
声明的全局变量,所绑定的顶层对象的属性的可配置性是否。
所以你不能重写其属性描述符,也不能用delete
来删除这个属性。
常量?
上一篇已经说过js中的不存在常量,而从这篇文章可以看出js存在“不可写属性”。
所以,如果你实在想要常量的话,可以声明一个全局对象,然后在这个对象上绑定不可写属性:
var CONST_OBJ = {}
Object.defineProperties(CONST_OBJ,{
a : {value:1,writable:false,configurable:false,enumerable:true},
b : {value:2,writable:false,configurable:false,enumerable:true}
})
这样,CONST_OBJ.a
和CONST_OBJ.b
都是不可修改的,可以当作常量使用。
不过在js里,给一个只读属性赋值,并不会有任何警告或者错误,只是无效操作而已:
CONST_OBJ.a = 3
CONST_OBJ.a //=>1
所以你完全不会意识到自己写错啦!
你应该开启ES5的严格模式来配合不可写属性,在这种模式下,给不可写属性赋值会报错:
'use strict'
CONST_OBJ.a = 3 //=>Uncaught TypeError
请注意如果你是在浏览器里试验上面代码的话,需要2行一起拷贝粘贴才能看出效果。
如何开启严格模式,以及严格模式别的作用,可以参考MDN的严格模式链接。
另,writable
为false的属性叫不可写属性,只有getter
的属性叫只读属性;两者都可以仿照成常量。
内省与反射
其实整篇文章中几乎都在讨论内省与反射,这正好是c++的弱点。
在计算机科学中,内省是指计算机程序在运行时(Run time)检查对象(Object)类型的一种能力,通常也可以称作运行时类型检查。一些编程语言如C++、Java、Ruby、PHP、Objective-C、Perl等等具有这种特性。
不应该将内省和反射混淆。相对于内省,反射更进一步,是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。一些编程语言比如Java具有反射特性,而C++不具有反射特性只具有内省特性。
想象你的程序拿到一个字符串str
,指定了a对象上要调用的方法。
在js中直接用方括号就可以将字符串转化成函数调用:a[str]
。
而在c++中,你不得不写一个巨大的if-else为字符串和函数建立映射:
void MyClass::CallFuncByName(std::string str)
{
if(str == "func1")
func1();
else if(str == "func2")
func2();
...
}
上面这个问题,其实很多没有学过编程或者从动态语言转而学习c/c++的人都会遇到。
这是c++缺乏反射能力的体现,对象并不知道自己身上有哪些方法。