JavaScript——对象
- 引言
- 正文
- 一、对象的定义
- 二、对象的创建
- 三、对象的原型以及原型链
- 四、对象的属性
- (1)属性的查询与设置
- (2)属性的删除
- (3)属性的检测
- (4)特殊的属性
- (5)属性的特性
- 五、对象的特性
- (1)对象的原型
- (2)对象的类
- (3)对象的扩展
- 结束语
引言
相信很多小伙伴学习了javascript,但是对于对象只是有一个初步的认识,并且对于对象的原型和原型链之类的概念还没有很好的认识吧,本篇文章带你们一步步了解对象。本篇文章一共13000 的字,我真的废了很多很多的心思,希望你们耐心的看完,并且能有深一层的理解。
- 公众号:前端印象
- 不定时有送书活动,记得关注~
- 关注后回复对应文字领取:【面试题】、【前端必看电子书】、【数据结构与算法完整代码】、【前端技术交流群】
正文
相信很多小伙伴都是冲着原型和原型链的概念进来的,那么你们可以先看正文的第二部分,然后再看第三部分,就能充分理解原型和原型链的概念了
一、对象的定义
对象是JavaScript的基本数据类型,对象内部是由一个个的名/值对组成的,例如下面我们常见的。
代码语言:javascript复制{
name: '张三',
fn: function() { return 1 }
}
在JavaScript中,对象一共有三类,分别是:
- 内置对象:是由ECMAScript定义的对象或类。例如数组 Array 、函数 Function 、日期 Date 、正则表达式 RegExp
- 宿主对象:由JavaScript解释器所嵌入的宿主环境定义的。例如浏览器提供的对象 Window 、Document
- 自定义对象:是由用户写的JavaScript代码创建的对象。例如
let obj = {}
二、对象的创建
创建对象的方式一共有三种,分别是:
- 对象直接量
- 通过new创建对象
- 通过Object.creat()创建对象
- 对象直接量
这种创建方式是我们最常见的,也是最常用的
代码语言:javascript复制let obj1 = {} //创建了一个空的对象
let obj2 = { //创建了有一个属性为name,值为张三的对象
name: '张三'
}
这种方式创建对象有一种缺点,比如在一个会重复调用的函数里,用了对象直接量的方式创建对象, 会重复创建很对的新对象,并且每次创建的对象的属性值也有可能不同。所以在实际应用中,如果遇到此类情况,尽量避免使用对象直接量的方式创建对象。
- 通过new创建对象
这种创建对象的方式,一般都是一个new运算符后面跟随一个函数调用,并且该函数有一个名字,叫做构造函数,顾名思义,就是用于构造创建一个对象。
代码语言:javascript复制let arr = new Array()
let data = new Date()
这几个都是通过new调用了一个内置对象的构造函数,创建了新的对象实例,我们其实也可以自己定义一个构造函数,然后也通过new的方式来调用我们自定义的构造函数来创建一个对象实例,例如
代码语言:javascript复制function MyObj() {
}
let obj = new MyObj() //成功调用构造函数 MyObj,创建了一个对象
- 通过Object.create()创建对象
这种方式是ES5中规定的新的创建对象的一种方式,只需要简单的传一个原型对象,即可创建对应的新的对象,例如
代码语言:javascript复制let obj = Object.create({x:1, y:2}) //{x:1, y:2}是obj的原型
三、对象的原型以及原型链
刚在讲解创建对象的方法时,我们在介绍Object.creat()
时,提到了原型的概念,在这里我们就来解释一下,什么是对象的原型。
每一个对象(除了null)都和另一个对象有关联,“ 另一个对象 ” 就叫做原型。 在这里我们可以做个形象的比喻,将原型比作一家餐饮店,将对象比作这家餐饮店的加盟店,这样理解起来就很容易了,我们创建了一个对象,就相当于我们开这家餐饮店的加盟店,并且我们的食材配方 、经营方法都是来自于这家加盟店,所以这里我们可以引入继承的概念,说是加盟店继承于这家餐饮店。
在JavaScript中,绝大部分的对象都有一个共同的原型,他就是 Object.prototype
,也就是说 Object.prototype
是最原始的那家餐饮店,而非加盟店。
每个函数的内部都有一个属性,叫做 prototype
,他表示该函数的原型对象,例如
//先写一个构造函数
function MyObj() {
//这里暂时先不写任何代码
}
console.log(typeof MyObj) // function
console.log(typeof MyObj.prototype) // Object
从这个例子中可以看出,MyObj.prototype 返回的是一个对象类型的值,所以这就表示了它是该构造函数的原型对象。
每个对象(除了null)都有一个属性——__proto__
,该属性表示的是该对象的原型,我们来举两个例子,这两个例子是分别不同方式创建对象后,展示他们各自的原型
- 用对象直接量创建的对象的原型
let arr = [] //对象直接量创建对象
console.log(arr.__proto__ === Array.prototype) // true
console.log(arr.__proto__.__proto__ === Object.prototype) //true
console.log(arr.__proto__.__proto__.__proto__) // null
arr.__proto__
表示的是arr这个对象的原型,而我们都知道对象直接量其实是一种语法糖的写法,在这个例子中 let arr = []
间接调用了 new Array
,所以我们可以通过 Array.prototype
来表示 Array 这个构造函数的原型对象, 将 arr.__proto__
和 Array.prototype
做比较,发现它俩相等,所以 arr 的原型就是构造函数Array的原型对象。
那么 arr.__proto__.__proto__
是为了表示 arr 的原型(Array.prototype)的原型,因为我们上面说过,绝大多数的对象都有一个共同的原型 Object.prototype
, 所以我们判断一下 Array.prototype
的原型是否就是 Object.prototype
,从结果来看,确实是的。
最后我们再去寻找 Object.prototype
的原型(arr.__proto__.__proto__.__proto__
)就找不到了,因为 Object.prototype
我们看作是一家餐饮店的源头啊,他并不是谁的加盟店,所以最终返回 null。
在这里我们就可以引入一个概念,叫做原型链。顾名思义,就是一条链子上有很多的原型,如图
- 通过Object.create()创建对象
let arr = Object.create({x:1, y:2})
console.log(arr.__proto__) // {x:1, y:2}
console.log(arr.__proto__.__proto__ === Object.prototype) // true
console.log(arr.__proto__.__proto__.__proto__) // null
在第三种创建对象方式中,我们说到,用Object.create()创建对象,只需要传入一个原型对象,就可以创建一个继承于该原型对象的新对象。 所以 arr 的原型(arr.__proto__
)就是我们传入的那个对象,即 {x:1, y:2}
。
arr.__proto__.__proto__
返回的是 {x:1, y:2}
的原型。我们都知道{x:1, y:2}
这样的对象是通过对象直接量创建的,所以他其实是通过 new Object()
来创建的对象, 那么 {x:1, y:2}
的原型就为 Object.prototype
了。
同样的, Object.prototype
没有原型,所以最后返回 null 。
此时的原型链是这样的,如图
四、对象的属性
定义: 一个对象内部的每个名/值对就是该对象的一个属性,例如 {x:1 ,y:2}
中, x:1
就是该对象的一个属性。
属性有两种类型:
- 自有属性: 直接定义在对象中的属性,例如
let obj = {x:1}
中,属性x就是该对象的自有属性 - 继承属性: 在对象的原型中定义的属性,例如
Object.prototype
中的属性toString
属性特性一共有四种:
- 值: 顾名思义,表示该属性的值
- 可写性: 表示是否可以设置该属性的值
- 可枚举性: 表示是否可以通过 for / in 循环返回属性的值
- 可配置性: 表示是否可以删除或修改该属性
注意:这里列举了 属性的类型
和 属性的特性
,在下面讲解属性的相关知识时,都会涉及到,所以大家请先尽力记住,这对下面的理解有帮助。
(1)属性的查询与设置
当我们要设置或者获取一个对象的属性时,我们一般都是这么做的
代码语言:javascript复制let obj = {
name: 'Lpyexplore',
age: 21,
gender: 0
}
let name = obj.name //获取obj对象中的name属性 Lpyexplore
let age = obj["age"] //获取obj对象中的age属性 21
let fn = obj.toString //获取obj对象中的继承属性toString [Function: toString]
obj["like"] = "python" //给obj对象设置一个like属性,值为python
obj.weight = 128 //给obj对象设置一个weight属性,值为128
obj.age = 22 //修改obj对象中的age属性,值改为22
console.log(obj.height) //获取obj对象中的height属性
console.log(obj) //查看一下obj对象的变化
/*
undefined
{
name: 'Lpyexplore',
age: 22,
gender: 0,
like: 'python',
weight: 128
}
*/
从这个例子中我们可以看到,查询一个对象的属性有两种方式,第一种是通过点(.)来访问到某属性,即 对象.属性名
; 第二种是通过方括号([ ])来访问某属性,即 对象[属性名]
。 当访问对象中不存在的属性时,会返回undefined
那么给对象设置一个属性值就更简单了,查询到该属性值后,直接给它赋值就可以设置属性值(若对象内不存在该属性)或修改属性值(对象内已存在该属性值)
(2)属性的删除
删除对象的自有属性,需要用到运算符 delete
,直接来看例子
let obj = {
x:1,
y:2,
z:3
}
delete obj.x //删除对象obj中的属性x,返回true
delete obj["y"] //删除对象obj中的属性y,返回true
delete obj.toString //toString是对象obj的原型中的属性,属于继承属性,无法删除该属性,但仍然返回true
console.log(obj.x) //查询对象obj中的属性x
console.log(obj) //查看对象obj
/*
undefined
{z:3}
*/
在这个例子中可以看到,我们准备删除对象obj中的继承属性toString时,未做任何操作,所以 delete 只能删除对象的自有属性
(3)属性的检测
我们有时需要检测对象的属性,即判断该对象中是否有某个属性 、该属性是否为该对象的自有属性 、该对象是否是可枚举的等等
- 通过 in 运算符判断属性是否存在
let obj = {
x: 1,
y: undefined
}
"x" in obj //返回true,表示对象obj中有属性x
"y" in obj //返回true,表示对象obj中有属性y,只不过值为undefined
"z" in obj //返回false,表示对象obj中不存在属性z
delete obj.x //删除对象obj中的x属性
"x" in obj //返回false,表示对象obj中不存在属性x
- 通过对象的 hasOwnProperty( )方法判断属性是否为自有属性
let obj = {
x:1
}
obj.hasOwnProperty("x") //返回true,表明属性x存在,且为obj的自有属性
obj.hasOwnProperty("z") //返回false,属性zu不存在于对象obj中
obj.hasOwnProperty("toString") //返回false,属性toString是obj的继承属性,不是自有属性
- 通过对象的 propertyIsEnumerable( )方法判断属性是否为自有属性,且该属性具有可枚举性
let obj = Object.create({x:1}) //新建一个对象obj,继承于对象 {x:1}
obj.y = 2 //给obj对象设置一个属性y,其值为2
obj.propertyIsEnumerable("y") //返回true,表示属性y为该对象的自有属性,且具有可枚举性
obj.propertyIsEnumerable("x") //返回false,因为属性x是继承属性,继承于对象{x:1}
Object.prototype.propertyIsEnumerable("toString") //返回false,虽然属性toString是对象Object.prototype的自有属性,但它不具有可枚举性
这里提到了可枚举性, 我们来举个例子
代码语言:javascript复制let obj = {
x:1,
y:2,
z:3
}
for(let i in obj) { //遍历对象obj的自有属性
console.log(i)
}
// 输出 1 2 3
可以看到对象obj的自有属性都被 for / in 全部遍历了出来,这就是可枚举性的体现。
其实在ES5中,提供了两个便利属性的函数,我们来了解一下
- Object.keys( )
这个函数是会返回一个数组,数组中的元素就是对象中可枚举的自有属性名,来看一下例子
代码语言:javascript复制let obj = {
x:1,
y:2,
z:3
}
Object.keys(obj) // ['x', 'y', 'z']
- Object.getOwnPropertyNames( )
这个函数与Object.keys() 类似,区别就在于,该函数返回的是对象中所有自有属性的名称,即不管属性是否具有可枚举性都能被返回。
代码语言:javascript复制let obj = {
x:1,
y:2,
z:3
}
Object.getOwnPropertyNames(obj) // ['x', 'y', 'z']
注意: 因为我们还没有讲到如何将一个属性变为可枚举或变为不可枚举,所以这两个函数的区别没办法很好的体现,接下来我们就来讲解一下如何设置属性的三个特性,其中也包括如何设置可枚举性。等了解完如何设置属性的可枚举性后,我们再来尝试一下这两个函数的区别,应该就很好理解了。
(4)特殊的属性
常见的对象属性一般都是名/值对的形式,即 x:1
这样的,我们把这种形式的属性叫做数据属性。在ES5中,提供了一种新的属性形式,叫做存储器属性,该属性可以用两种方法定义,他们分别是 getter 和 setter ,存储器属性在对象中的存在形式不是名/值对的样子,而是类似于我们平时定义函数的样子,function fn() {}
。定义存储器属性就是用get(getter)或set(setter) 代替关键字 function,fn就是该属性的名字,有点抽象,来看例子吧
let obj = {
x:1,
y:2,
get r() { //用getter方法定义了属性r,在查询该属性时,调用该函数
return this.x 2
},
set r(data) { //用setter方法定义了属性r,在给属性r赋值时,调用该函数,并将值作为该函数的参数
this.y * data;
return this.y
}
}
let r = obj.r //查询对象obj中的属性r,返回 3
obj.r = 7 //给对象obj的属性r赋值为7, 返回 14
从上面这个例子中可以得出以下的结论
- 用getter方法定义了属性,在查询该属性值时,会调用getter方法定义的函数名为该属性的函数
- 用setter方法定义了函数,在给该属性赋值时,会调用setter方法定义的函数名为该属性的函数
- 通过前两条结论,可以知道,如果一个存储器属性具有getter方法,则该属性可读;如果具有setter方法,则该属性可写;同时拥有两个方法的话,则该属性是一个可读写的属性。
(5)属性的特性
在第四部分的开头,我们说了属性有四个特性,即值 、可写性 、可枚举性 、可配置性,忘记了的小伙伴翻到前面再看一下。
一般我们创建的数据属性,都是具有这四个特性的(值 、可写性 、可枚举性 、可配置性), 存储器属性是不具有值和可写性两个特性的,但他也具有四个特性,他们分别为:读取(get)、写入(set)、可枚举性 和 可配置性。
在这里我们先引入一个概念,也是ES5定义的一个对象,叫做属性描述符,这个对象就代表了属性的四个特性。属性描述符对象里的属性有 value(值)
、writable(可写性)
、enumerable(可枚举性)
、configurable(可配置性)
、get(读取)
、set(写入)
通过 Object.getOwnPropertyDescriptor( )
可以获得一个对象中某个属性的属性描述符。该方法第一个参数为对象,第二个参数为需要查询的该对象中的属性名。接下来我们来实战一下
- 查看数据属性的属性描述符
//先创建一个对象obj
let obj = {
x:1
}
Object.getOwnPropertyDescriptor(obj, "x") //查询对象obj中属性x的属性描述符
// 返回 {value: 1, writable: true, enumerable: true, configurable: true}
- 查看存储器属性的属性描述符
//先创建一个对象obj
let obj = {
get r() {
return this.x 1
},
set r(data) {
this.x = data
}
}
Object.getOwnPropertyDescriptor(obj, "r") //查询对象obj中属性r的属性描述符
// 返回 {get: [Function: get r], set: [Function: set r], enumerable: true, configurable: true}
getOwnPropertyDescriptor()
只能获取到一个属性的属性描述符,如果我们想要修改某个属性的特性的话,我们需要用到另一个方法,即 Object.defineProperty()
,他的第一个参数是对象;第二个参数是需要创建或者修改的属性名;第三个参数是属性描述符对象。
直接来看两个实战例子
- 修改对象中属性的特性
//创建一个对象
let obj = {
x:1
}
//先用propertyIsEnumerable()来测试一下对象obj中的属性x是否还具有可枚举性
obj.propertyIsEnumerable("x") //返回true,说明此时属性x是具有可枚举性的
//修改对象obj中属性x的属性特性
Object.defineProperty(obj, "x", {
value: 2, //属性x的值变为2
writable: true, //属性x具有可写性
enumerable: false, //属性x不具有可枚举性
configurable: true //属性x具有可配置性
})
//利用 propertyIsEnumerable()来测试一下对象obj中的属性x是否还具有可枚举性
obj.propertyIsEnumerable("x") // 返回 false,说明对象obj中属性x已经不具有可枚举性了
- 给对象创建一个属性,并设置该属性的特性
//创建对象obj
let obj = {
x:2
}
//给对象obj创建一个属性,并配置好该属性的特性
Object.defineProperty(obj, "r", {
get: function() {return this.x 1}, //给存储器属性r设定一个get函数
set: function(data) {this.x *= data}, //给存储器属性r设定一个set函数
enumerable: true, //存储器属性r具有可枚举性
configurable: true //存储器属性r具有可配置性
})
//查询对象obj中的属性r
obj.r // 返回 3
//给对象obj中的属性r赋值
obj.r = 3
//查询对象obj中的属性x的值
obj.x //返回 6
- 给对象同时创建多个属性,并为每个属性配置属性特性
这里要用到另一个方法,即 Object.defineProperties()
,这个方法跟 Object.defineProperty()
类似。前者一共有两个参数,第一个参数为对象;第二个参数为一个对象,并且该对象内部是以名/值对的形式存在的,即 需要修改的属性名: 属性描述符对象
。接下来我们直接来看实战例子
let obj = {
x:1
}
Object.defineProperties(obj, {
x: {
value: 3,
writable: true,
enumerable: true,
configurable: true
},
y: {
value: 10,
writable: true,
enumerable: true,
configurable: true
}
})
console.log(obj)
// 返回 {x:3, y:10}
在这里简单总结一下
Object.defineProperty()
这个方法有两个作用,第一个是修改属性的特性;第二个作用就是给一个对象创建属性Object.defineProperty()
和Object.getOwnPropertyDescriptor()
可以搭配着使用,尤其是在你不知道一个属性的特性的时候,可以先用后者获取它的属性描述符对象,再根据属性描述符对象调用前者去修改属性的特性。- 无论是
Object.defineProperty()
还是Object.defineProperties()
,他们都只能修改对象的自有属性,无法修改他们的继承属性。
五、对象的特性
对象一共有三个对象特性,他们分别是:
- 对象的原型: 每个对象(除了null)都与另一个对象相关联,并且继承另一个对象的属性或方法。
- 对象的类: 是一个标识对象类型的字符串
- 对象的扩展标记: 指明了是否可以向该对象添加新的属性
(1)对象的原型
在上面我已经花一定的篇幅去介绍了对象的原型了,这里就不再做过多的介绍了。这里再介绍两种判断原型的方法,第一个是 Object.getPrototypeOf()
,第二个是 ifPrototypeOf()
- Object.getPrototypeOf( )
这个方法是ES5新增的方法,它需要传入对象作为参数,然后就会返回这个对象的原型,来看一下例子
代码语言:javascript复制let obj = Object.create({x:1}) //新建一个对象obj继承于 {x:1}
Object.getPrototypeOf(obj) //返回 {x:1}
- isPrototypeOf( )
该方法可以判断一个对象是否是另一个对象的原型,或者说在另一个对象的原型链上。 来看一下例子
代码语言:javascript复制let obj1 = {x:1}
let obj2 = Object.create(obj1)
obj1.isPrototypeOf(obj2) //返回true,因为obj2就是继承于obj1
Object.prototype.isPrototypeOf(obj2) //返回true,因为Object.prototype在obj2的原型链上
(2)对象的类
对象也是有类型的,这个我在本文的开头也就列举了,对象一共有这三大类,分别是内置对象 、宿主对象 、自定义对象。
我们如何来区分对象的具体类型呢?其实在 Object.prototype
中定义了一个方法属性,叫做 toString,调用该方法,并传入一个对象,就会返回一个字符串,字符串里的信息就用以表示对象的类型。
我们直接通过 Object.prototype.toString.call(obj)
来判断对象的类型,这里用到的 call
的作用就是将 toString 方法内部的 this 指向我们要判断的对象,如果有不懂的小伙伴可以去查看一下我的另一篇介绍call的文章
//先将 Object.prototype.toString.call() 封装成一个函数,方便后面的代码简化
function classof(obj) {
return Object.prototype.toString.call(obj)
}
classof(null) //[object Null]
classof(undefined) //[object Undefined]
classof(1) //[object Number]
classof("") //[object String]
classof(true) //[object Boolean]
classof({}) //[object Object]
classof([]) //[object Array]
classof(/d /) //[object RegExp]
classof(new Date()) //[object Date]
classof(window) //[object window]
classof(document) //[object HTMLDocument]
function a() {}
classof(new a()) //[object Object]
let b = {}
classof(b) //[object Object]
let c = Object.create(b)
classof(c) //[object Object]
简单总结一下
- 继承于内置对象的对象,会返回一个字符串,字符串里包含了该内置对象的构造函数名
- 判断宿主对象的类型时,例如判断window对象的类型,需要在浏览器环境下才能判断。
- 通过对象直接量 、调用构造函数(new fn())以及Object.create() 这三种方式创建的自定义对象,他们的对象类型都为
[object Object]
(3)对象的扩展
对象是具有扩展性的,其表示能否给对象添加新的属性。一般情况下,对象都是可扩展的,除非我们将他转换成了不可扩展的。接下来我们来看几个将对象转换成不可扩展的方法。
- Object.preventExtensions( )
我们要想将一个可扩展的对象转换成不可扩展的,我们需要调用 Object.preventExtensions()
,将需要转换的对象作为参数传入即可。
注意:
- 对象一旦转换成不可扩展,就无法再转成可扩展的了
- 对象转换成不可扩展,只会影响该对象本身,但不会影响原型。即无法给该对象添加新的属性,但是我们可以给该对象的原型添加新的属性,该对象可以继承原型中的新属性。
好了废话不多说,接下来看一个例子
代码语言:javascript复制let obj1 = {x:1}
let obj2 = Object.create(obj1) //obj2继承于Obj1
obj2.z = 4 //给obj2添加一个属性z,值为4
console.log(obj2) //打印 {z:4}
Object.preventExtensions(obj2) //将obj2转换成不可扩展的
obj2.y = 2 //尝试向obj2中添加新属性y
console.log(obj2.y) // 打印 undefined ,说明obj2是不可扩展的
obj1.y = 2
console.log(obj2.y) // 打印 2 ,说明obj2虽然被转换成不可扩展的了,但是还是能从原型继承属性的
delete obj2.z //打印 {},说明使用Object.preventExtensions()将对象转换成不可扩展后,虽然无法添加属性,但是可以删除属性
我们可以将对象传入 Object.isExtensible()
来判断对象是否为可扩展的。
- Object.seal( )
该方法是ES5中提供的,不仅具有能将对象转换成不可扩展的作用(无法添加新属性),而且还能使对象中的自有属性都设置为不可配置的,即无法删除自有属性了。我们可以成为将对象封闭。
代码语言:javascript复制let obj1 = {x:1}
let obj2 = Object.create(obj1) //obj2继承于Obj1
obj2.z = 4 //给obj2添加一个属性z,值为4
console.log(obj2) //打印 {z:4}
Object.seal(obj2) //将obj2封闭
delete obj2.z //打印 {z:4},说明使用Object.seal()将对象封闭后,既无法添加属性,也无法删除属性
obj2.z = 100 //修改obj2中的属性z为100
console.log(obj2) //打印 {z:100} ,说明已封闭的对象,还是可以对其属性进行修改的
我们可以将对象传入 Object.seal()
中,判断对象是否已封闭
- Object.freeze( )
该方法也是ES5中提供的,它在 Object.seal()
的基础之上,增加了一个限制,即将对象的自有的所有数据属性设置为只读。我们把这种限制叫做冻结
let obj1 = {x:1}
let obj2 = Object.create(obj1) //obj2继承于Obj1
obj2.z = 4 //给obj2添加一个属性z,值为4
console.log(obj2) //打印 {z:4}
Object.freeze(obj2) //将obj2冻结
obj2.z = 100 //修改obj2中的属性z为100
console.log(obj2) //打印 {z:4} ,说明已冻结的对象,无法修改对象中的属性值
我们可以将对象传入 Object.isFrozen()
中,判断对象是否已冻结
结束语
好了,洋洋洒洒一整篇文章也写完了,写加上思考理解大概耗时有十几个小时吧,说真的,刚开始写这篇文章的时候,自己对于 “对象” 这个概念也不是说很透彻很透彻,但是我为了让大家能明白,自己也花了很多的心思去研究,去理解。到现在完结,我对 “对象” 的概念也是有了更深的理解了。 真的是原创不易啊,大家觉得写的不错的点个关注,点个赞,感谢啦~