从c++到javascript(2)——作为数据的对象

这篇博客探讨了JavaScript中的对象,将其视为有行为的数据结构。内容包括对象字面量、属性与方法、setter与getter,强调了JavaScript对象与C++中的类对象的相似性和区别,以及如何在JavaScript中实现类似于常量的功能。此外,还讨论了内省和反射在动态语言中的重要性。
摘要由CSDN通过智能技术生成

js并不是原生支持面向对象的,面向对象在js中用离散、繁复的语法表示出来。
我觉得如果把单个对象从整个对象模型中拿出来讨论,可以让人更容易接触js中对象的本质,即一种有行为的数据结构。


单个对象

对象在宏观上讲,就是将一组数据和对其进行操作的函数封装在一起的“一坨”东西。

对象字面量

对象字面量是封闭在花括号对({})中的一个对象的零个或多个”属性名-值”对的(元素)列表。

属性名和值之间用:连接,元素之间用,分隔

下面通过定义一个对象字面量并赋值给变量,创建了一个student对象:

var student = {
    name:'Jack',
    id:'12306',
    grade:99
};

对象是什么

我们这里先不谈面向对象,因为面向对象是很多对象的组合设计,这里我们只讨论单个对象。

如果按数据结构来看的话,

js的对象就是键值对的合集。

有些像是c++里的structstd::map的混合体。
不过struct里的成员不可改,std::map是个同质容器(元素都是同类型的)。

而如果从含义上来看的话,js和c++里的类的对象都是一样的,即表示为一个有自己行为的个体。

属性与方法

属性就是附加到对象上的变量。

在上一节的例子里,student对象分别有3个属性:name id grade

提到变量,我们之前说过:

  1. 在js里只有1种非原始类型,即对象
  2. 对象可以赋值给变量
  3. 函数也是对象

自然,函数也可以赋值给变量。
所以,属性也可以是函数

函数类型的属性也称为方法。

在c++中,术语有些不同:
成员变量对应js的不是函数类型的属性,成员函数对应js的方法。

setter与getter

c++里,一个成员变量要改变取值和赋值的行为,可以通过重载运算符的方式达到。
js中没有重载运算符,但提供了一套专用的语法,setget

当尝试设置属性时,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个键的属性描述符将被认为是数据描述符。

这是它们的键表:

形式可配置性可枚举性可写性getset
数据描述符
存取描述符

查询属性描述符

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.aCONST_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++缺乏反射能力的体现,对象并不知道自己身上有哪些方法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值