欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

前端入门10-JavaScript语法之对象

程序员文章站 2022-03-29 19:45:08
声明 本系列文章内容全部梳理自以下几个来源: 《JavaScript权威指南》 "MDN web docs" "Github:smyhvae/web" "Github:goddyZhao/Translation/JavaScript" 作为一个前端小白,入门跟着这几个来源学习,感谢作者的分享,在其基 ......

声明

本系列文章内容全部梳理自以下几个来源:

作为一个前端小白,入门跟着这几个来源学习,感谢作者的分享,在其基础上,通过自己的理解,梳理出的知识点,或许有遗漏,或许有些理解是错误的,如有发现,欢迎指点下。

ps:梳理的内容以《javascript权威指南》这本书中的内容为主,因此接下去跟 javascript 语法相关的系列文章基本只介绍 es5 标准规范的内容、es6 等这系列梳理完再单独来讲讲。

正文-对象

在 javascript 除了原始数据类型外,其余均是对象,函数是对象,数组也是对象;继承通过对象来实现,构造函数也通过对象来实现,所以对象在 javascript 里有着很重要的角色,理解和掌握对象的一些特性,对于掌握 javascript 这门语言有着很大的帮助。

java 里对象有属性和方法之分,但在 javascript 中,只存在属性,变量是属性,方法也是属性,对于 javascript 来说,对象,其实只是一堆属性的无序集合而已,外部可通过对象来操作各种属性,只不过有的属性,它的值是函数类型,所以这时可叫它为对象的方法。

对象的每个属性,都是一个 key-value 的形式,属性名和属性值。而属性,又分自有属性和继承属性,自有属性是指对象本身自己拥有的属性,而继承属性是指继承的属性。

对象分类

一般来说,有三类对象,分别是内置对象、宿主对象、自定义对象:

  • 内置对象:是指语法标准规范中内置实现的一些对象,例如函数、数组、正则、日期等这些内置对象;
  • 宿主对象:是指 javascript 解释器所嵌入的宿主环境,在前端里,一般来说宿主环境就是浏览器,浏览器也会定义一些内置对象,比如 htmlelement 等;
  • 自定义对象:开发人员自行实现的对象。

创建对象

创建对象有三种方式:对象直接量、构造函数、object.create()

对象直接量

这是最简单的一种创建对象的方式,在代码中,直接通过 {} 形式创建一个对象,如:

var book = {
    "main title": "javascript",
    'sub-title': "the definitive guide",
    "pages": 900,
    author: {
        firstname: "david",
        surname: "flanagan"
    }
};

上述代码中,等号右侧 {} 代码块形式定义的对象方式,就叫对象直接量。代码中,每出现一次对象直接量,会直接创建一个新的对象,对象的属性就是对象直接量中定义的。

定义属性时,有几点需要注意一下,属性名也就是 key 值,可加引号也可不加引号,但如果属性名使用到一些保留字时,就肯定需要加引号。

属性值可以是 javascript 中的任意类型,原始类型、对象类型都可以。

构造函数

构造函数就是通过 new 关键字和函数一起使用时,此时的函数就称构造函数,用途在于创建一个新的对象。具体在后续讲函数时会详细讲解。

这里可以看个例子:

var o = new object();
var o1 = new object;
var a = new array();

function book() {}
var b = new book();

通过 new 关键字和函数一起使用时,就可以创建新对象,例子中的 object 和 array 是内置的构造函数,也可以自定义构造函数,其实就是自定义一个函数,让它和 new 关键字一起使用就可以了。

通过构造函数方式如何给新创建的对象添加一些初始化的属性,这些细节和原理在函数一节中再讲,这里需要注意一点的就是,当不往构造函数传参数时,此时括号是允许可以省略的。

另外,第一种对象直接量的方式创建对象,其实,本质上也是通过构造函数:

var o = {name:"dasu"} 
//等效于
var o = new object();
o.name = "dasu";

对象直接量其实是一种语法糖,可以通俗的理解,javascript 为方便我们创建对象,封装的一层工具,其内部的本质实现也是通过构造函数。

object.create()

你可以把 object.create() 理解成 java 中的静态方法。

通过这种方式,可以创建一个新的对象,参数是指定对象要继承的目标对象,这个被继承的对象,在 javascript 里被称为原型。

举个例子:

var o = object.create(new object());  //创建一个新对象,让它继承自一个空对象

通过构造函数创建的对象,其继承关系是由构造函数决定的,而 object.create() 方式,可自己手动指定继承关系。当然,并不是说,构造函数就无法自己指定继承关系。

原型

原型可以理解成 java 中的父类概念。

在 javascript 中,对象也存在继承关系,继承的双方都是对象,对象是从对象上继承的,被继承的那个对象称作原型。所以,有一种描述说,javascript 是基于原型的继承。

在 java 中,是通过 extends 关键字实现继承关系,那么在 javascript 里呢?

自然也有类似的用来指定对象的继承关系,这就取决于创建对象的方式,上面说过,创建对象有三种方式:对象直接量、构造函数、object.create(),但由于对象直接量本质上也是通过构造函数,所以其实就两种。

对于构造函数创建的对象来说,因为每个函数都有一个 prototype 属性,prototype 是它的属性名,属性值是一个对象,这个对象就是原型,就是通过该构造函数创建出来的新对象的继承来源。

我们可以通过修改构造函数的 prototype 属性值来达到指定对象继承关系的目的,如果不修改,那么内置的构造函数如 object 或 array 这些都已经有默认指定的 prototype 属性值了,也就是创建内置对象时,这个对象已经具有一定的默认继承结构了。

对于 object.create() 方式创建对象,参数传入的就是子对象的原型,想让创建出来的对象继承自哪里,就传入那个对象就可以了。这个方法必须传入一个参数,否则运行时会抛异常,但可以传入 null,表示不继承任何对象,所以,javascript 里,是允许对象没有原型,允许对象不具有继承结构的。

对于原型,在后续会专门有一篇来讲讲,这里大概清楚些概念即可。

添加属性

javascript 里的对象,其实可以通俗的理解成属性的集合,既然是作为容器的存在,那么其实创建完对象只是第一步,后续就是往这个集合中添加属性,所以 javascript 里,对象是允许在运行期间动态添加属性的。

添加属性的方式,可以通过对象直接量方式,在创建对象之时,就写在对象直接量中,或者运行期间动态添加,如:

var o = {name:"dasu"}
o.age = 24;
o.sex = "man";
o.love = "girl";

前端入门10-JavaScript语法之对象

但需要注意一点的是,不像 java 中在编写类代码,为类添加成员变量时,可以只声明却初始化。在 javascript 中,是不允许这样的。

也就是说,为对象添加某个属性时,需要直接将属性名和属性值都设置好,其实想想也能明白,对象无非就是属性的集合,你见过对哪个集合进行添加数据时,是可以只对该集合设置一个 key 值的吗?

查询属性

访问对象的属性方式很简单,两种:. 运算符或 [] 运算符;

两种方式都可以访问对象的属性,但有一个区别:

  • . 运算符访问属性的话,后面跟着的是属性名
  • [] 运算符访问属性的话,中括号内跟着的是属性名的字符串

仍旧以上面例子中的代码为例:

前端入门10-JavaScript语法之对象

由于通过 [] 运算符访问对象的属性,需要的是一个属性名的字符串形式,所以这种方式会特别灵活,你可以再 [] 内放一个表达式也可以,只要表达式最后的结果是字符串,或者说可以自动类型转换为属性名的字符串即可,特别灵活。

. 运算符可能会比较习惯,但它就只能明明确确的通过属性名来访问了,如果你想通过某种拼接规则来生成属性名,就只能用 [] 不能使用 .

如果访问对象中某个不存在的属性时,并不会抛异常,会输出 undefined,但如果继续访问不存在的属性的属性时,等价于访问 undefined 原始类型值的属性,这就会抛异常了:

前端入门10-JavaScript语法之对象

ps:是不是发现,对对象的操作很像 java 中对集合的操作?所以,有人解释说对象是属性的集合,这不是没根据的。

删除属性

delete 是用来删除对象上的属性的,但它只是将该属性名从对象上移除,并不删除属性值,举个例子:

var a = [1,2,3];
var o = {name:"dasu", arr:a};

delete o.arr;
console.log(a[0]);  //输出 => 1
console.log(o.arr); //输出 => undefined

新键一个对象 o,它有个属性 aar 存储着数组 a,当通过 delete 删除对象 o 上的 aar 属性后,再去访问这个 aar 属性,获取的是 undefined,表明这个属性确实被删除了,但本质上只是将其与这个对象 o 的关联删除掉,并不会删除属性值,所以输出数组 a 的值时还是可以访问到的。

不过,delete 有一些局限,它并不是什么属性都可以删除:

  • 只能删除*属性,无法删除继承属性
  • 不能删除那些可配置性为 false 的属性

属性拥有一些特性,在下面会讲,其中有一个是可配置性,当将这个特性设置为 false 时,就无法通过 delete 来删除。

而之前说过的,通过 var 声明的全局变量,虽然它最后是作为全局对象的属性存在,但它的可配置性被设为 false,所以这些全局变量才无法通过 delete 被删除。

尝试删除那些无法删除的属性,并不会让程序出问题,delete 表达式有一个返回值,true 表示删除成功,false 表示删除失败,仅此而已,没有其他什么副作用。

检测属性

因为 javascript 中对象的属性太过动态性了,在运行期间,都无法确定某个属性到底存不存在,某个到底是不是指定对象的属性,所以这种场景,一般都需要进行属性的检测。

也就是检测对象内是否含有某个属性,有多种方式,下面分别来介绍:

查询属性的方式

之前说过,访问对象内不存在的属性时,会返回 undefined,可以利用这点来判断对象是否含有要访问的属性。

这种方式有个缺点,如果属性值刚好被人为的手动设置成 undefined 时,就无法区别对象究竟有没有这个属性。

in 运算符方式

in 运算符左侧是属性名的字符串格式,右侧是对象,当右侧对象含有左侧字符串同名的属性时,返回 true,用这种方式就可以很好的判断对象是否含有某个属性。

前端入门10-JavaScript语法之对象

注意,左侧必须是属性名的字符串格式,跟 [] 运算符访问对象属性一样的限制要求。

但这种方式有个局限,就是无法区分这个属性究竟是自有属性还是继承属性,也就是说,继承自原型的属性通过该操作符同样会返回 true。

hasownproperty()

上面说过,通过构造函数创建的对象,默认都会存在内置的继承结构,不管什么对象,这个默认的继承结构顶端都是构造函数 object 的 prototype 属性值,由于它的属性值是一个内置的匿名对象,所以,通常都直接这么表达,对象都会继承自 object.prototype,直接用 object.prototype 的描述来代表这个属性所指向的具体对象。

所以,以后在看到诸如某某对象继承自 function.prototype 或 array.prototype 之类的描述,我们要能够清楚,它表示的是,对象的原型是 xxxx.prototype 这属性所指向的具体对象。

object.prototype 属性值指向的对象中,定义了一个 hasownproperty() 方法,所以基本所有对象都可以使用,它是用来判断,对象是否含有指定的自有属性的。

前端入门10-JavaScript语法之对象

首先利用上小节介绍的 in 方式来检测,o 对象的默认继承结构顶端是 object.prototype,所以 o 对象继承了它的 hasownproperty 属性,第一行代码返回 true。

这个 hasownproperty 属性是个方法,调用它可以来检测对象是否含有指定的自有属性,参数也需要传入属性名的字符串格式,所以第二行代码返回 false,第三行返回 true。

hasownproperty 是继承自 object.prototype 的属性,由于 hasownproperty() 方法只能检测自有属性,所以第四行返回 false。

propertyisenumerable()

这个方法同样是 object.prototype 中所定义的方法,所以,同样基本所有对象都能够使用。

它是 hasownproperty() 的增强版,也就是,用于检测对象的自有属性且该属性是可枚举性的,才会返回 true。

可枚举性是属性的另一个特性,用来标识该属性是否允许被遍历,下面会讲解。

因为有一些内置属性是不希望被枚举出来的,所以可通过该方法来判断。

遍历属性

遍历属性也称枚举属性,也就是类似于对集合进行遍历操作,将其所含有的属性一个个读取出来。

遍历对象属性的方式也有多种,也一一来介绍:

for-in 遍历

var o = {name:"dasu"}
var o1 = object.create(o);  //o1 继承自 o
o1.age = 24;
o1.sex = "man";
o1.love = "girl";

for(p in o1) {
    console.log(p);    
}

看看输出的结果:

前端入门10-JavaScript语法之对象

o1 继承自 o,在 o1 内有三个自有属性,有一个继承属性,通过 for-in 方式遍历对象 o1 的属性时,不管是自有属性,还是继承属性,都会被输出。

同时,输出的是属性名,并不是属性值,所以 for-in 方式只是遍历对象的属性(包括继承属性),并返回属性名,注意是属性名。

通常 for-in 这种方式,可以结合 hasownproperty() 方法一起使用,来过滤掉继承的属性。

object.keys()

这又是一个类似静态方法的存在,注意这个方法跟上述 object.create() 都是构造函数 object 上的方法,而普通对象继承的是构造函数 object.prototype 属性值所指向的那个原型对象,这是两个相互独立的对象,也就是说,通过构造函数创建出来的子对象并不是继承构造函数对象本身。

所以在子对象中,无法使用 object.keys() 这类构造函数对象本身的属性,这点需要注意一下,在后续专门讲继承时会再拿出来讲讲。

前端入门10-JavaScript语法之对象

参数传入需要遍历属性的对象,通过该方法,可以获得一个数组对象,数组内就存储着参数传入的对象的自有属性且属性是可枚举性的,相当于 for-in 方式结合 hasownproperty() 的效果。

object.getownpropertynames()

该方法也是遍历对象的自有属性,只是它是将参数传入的对象所拥有的所有属性都输出,包括那些被设置为不可枚举的属性,看个例子:

前端入门10-JavaScript语法之对象

object.prototype 指向了一个内置的对象,内置对象中定义了很多属性,继承这个原型的子对象们都可以使用这些属性,但这些属性都被设置为不可枚举性,所以通过 object.keys() 遍历它时,得到的是一个空数组,子对象通过 for-in 方式遍历时也读取不到这些属性。

这种设计是好的,但考虑到如果有某些场景是需要读取对象自身的所有属性,包括那些不可枚举的,此时,就可通过 object.getownpropertynames() 来达到目的了。

属性的特性

上面介绍中,或多或少有提到属性的特性,属性特性是指,属性的一些特有行为。

属性的特性一共有三个:可写性、可配置性、可枚举性

  • 可写性:表示这个属性是否允许被更改,当设置成 false 时,这就是一个只读属性
  • 可配置性:表示这个属性是否允许被动态的添加或删除,当设置成 false 时,就不允许通过 delete 来删除
  • 可枚举性:表示这个属性是否允许在遍历属性过程中被读取,当设置成 false 时,通过 for-in 或 object.keys 都无法遍历到这个属性

那么,如果知道对象的某个属性的这三种特性都是什么配置呢?

针对这种情况,内置了一个叫做属性描述符的对象,这个对象本身含有四个属性来描述属性:value、writable、enumerable、configurable。

  • value:描述属性值,即 key-value 中的 value
  • writable:描述属性的可写性
  • enumerable:描述属性的可枚举性
  • configurable:描述属性的可配置性

用来描述属性的数据结构有了,接下去就是如何操作了,先看一下,如果获取对象某个属性的描述信息:

object.getownpropertydescriptor()

还是通过object 的一个方法,接收两个参数,第一个参数是对象,第二个参数是对象内的某个自有属性,将会返回一个属性描述符对象:

前端入门10-JavaScript语法之对象

内置对象的很多属性都会针对属性的使用场景进行了不同的配置了,比如 object.prototype 中所有属性的 enumerable 可枚举性都配置成 false。

但对于在代码中,通过对象直接量创建的对象,或者自定义构造函数创建的对象等,对这些非内置对象添加的属性,默认这三个特性都为 true,即对象添加的属性默认都是可写、可枚举、可配置的。

object.getownpropertydescriptors()

这个方法也是用来获取对象属性的描述信息的,只是它只需一个参数即可,就是对象,然后会输出所有自有属性的描述信息:

前端入门10-JavaScript语法之对象

这两个方法都是只能获取对象的自有属性的描述信息,如果想要获取继承属性的描述信息,需要先获取原型对象,再调用这两个方法处理原型。获取原型对象后续讲原型时会介绍,这里知道思路就可以了。

object.defineproperty()

有获取对象属性的描述信息的方法,自然有设置对象属性的描述信息方法,所以与上面两个方法相对应的就是 object.defineproperty() 方法和 object.definproperties()。

object.defineproperty() 接收三个参数,第一参数是对象,第二个参数是需要修改属性描述信息的属性,第三个参数是含有属性描述符结构的对象:

var o = {name:"dasu"}
var o1 = object.create(o);  //o1 继承自 o
o1.age = 24;
o1.sex = "man";
o1.love = "girl";

object.defineproperty(o1, "age", {writable:false});
object.defineproperty(o1, "love", {enumerable:false, configurable:false});

第三个参数,你可以将四个属性值都指定,没指定的仍旧会使用默认的配置,再用 object.getownpropertydescriptors() 看下修改后的配置:

前端入门10-JavaScript语法之对象

object.defineproperties()

这方法作用跟上面一样,只是它是批量处理的方法,接收两个参数,第一个是对象,第二个是需要修改的属性集合,如:

var o = {name:"dasu"}
var o1 = object.create(o);  //o1 继承自 o
o1.age = 24;
o1.sex = "man";
o1.love = "girl";

object.defineproperties(o1, {
    age: {writable:false},
    love:  {enumerable:false, configurable:false}
})

规则

有一些规则需要注意一下:

  • 如果属性是不可配置的,那么不能修改它的可配置性和可枚举性,对于可写性,只能将 true 改为 false,不能将 false 改为 true
  • 如果属性是不可配置且不可写的,那么不能修改这个属性的值
  • 如果属性是可配置但不可写的,那么可以先将属性修改成可写,这时就可以修改属性值

属性的setter和getter

正常来说,对象的属性由属性的三种特性来控制属性的操纵限制,但有一种情况是例外的,那就是通过 setter 和 getter 添加的属性,这类属性通常叫做存取器属性,为了区分,将正常使用的那些属性叫做数据属性。

之所以叫做存取器属性,是因为,通过这种方式添加的属性,它的读写是交由 setter 和 getter 控制,并不是由属性描述符的三种特性控制。

先来看下,如何定义一个存取器属性:

var o = {
    set name(value) {},
    get name() {return "dasu"},
    
    get age() {return 24}
}

虽然看起来有点像 java 中的 set 方法和 get 方法,但完全是两种不一样的东西,首先,这里的 set 和 get 虽然类似方法,但外部是不能通过方法来调用,第二,外部访问这些存取器属性,仍旧是使用 .[] ,如 o.age 或 o["name"]。

相比于数据属性,存取器属性的区别就在于,读和写是通过 set 和 get 控制,在定义存取器属性时,如果没有定义 get,那么这个属性就是无法读取的,如果没有定义 set,那么这个属性就是不可写的。其余的,可枚举性和可配置性都跟数据属性一样。

也一样是通过 object.defineproperty() 和 object.getownpropertydescriptor() 来设置或查看存取器属性的描述信息,唯一需要注意的是,对于数据属性,描述符对象有四个属性:value,writable,enumerable,configurable;但对于存取器属性来说,没有 value 和 writable 属性,与之替换的是 get 和 set 两个属性,所以看个例子:

var o = {
    set name(value) {},
    get name() {return "dasu"},  //存取器属性,可读,可写,但读写逻辑得自己实现
    
    get age() {return 24},  //存取器属性,只读,读的逻辑得自己写
    
    sex: "man"  //数据属性
}

假设定义了这么个对象,有两个存取器属性,一个数据属性,通过 object.getownpropertydescriptors() 看一下这些属性的描述:

前端入门10-JavaScript语法之对象

所以存取器属性和数据属性就在于读和写这两方面的不同,看下修改描述的方式:

前端入门10-JavaScript语法之对象

存取器属性是可以换成数据属性,同样,数据属性也是可以换成存取器属性的,通过 object.defineproperty() 在修改属性描述信息时,使用的如果是 set 和 get,那就将数据属性换成存取器属性了,使用的如果是 value 和 writable,原本如果是存取器属性,就将存取器属性转换成数据属性了。

另外,它也有一些规则需要注意一下:

  • 如果存取器属性是不可配置的,则不能修改 set 和 get 方法,也不能将它转换为数据属性
  • 如果数据属性是不可配置的,则不能将它转换为存取器属性

对象的特性

对象的属性有它的几种特性,而对象本身也有一些特性,主要是三个:原型属性、类属性、可扩展性

原型属性:表示对象继承自哪个对象,被继承的对象称为子对象们的原型

类属性:表示对象的类型信息,是一个字符串,比如数字的类属性为 number

可扩展性:表示是否允许对象可动态的添加属性

原型留着后续讲原型时再来细讲,大概清楚对象是有继承结构,被他继承的对象称作它的原型,所以通常说 javascript 是基于原型的继承这些概念即可。

类属性

类属性,本质上就是通过调用 object.tostring() 方法来输出对象的一些信息,这些信息中,包括了对象所属类型的信息,对这串文本信息进行截取处理,就可以只获取对象所属类型的信息,所以称这些信息为对象的类属性。

类属性所呈现的信息很类似于 typeof 运算符所获取的信息,只是类属性会比 typeof 更有用一些,它能够区分所有的内置对象,以及区分 null,这些是 typeof 所做不到的,如:

前端入门10-JavaScript语法之对象

可扩展性

类似于属性有可配置性、可写性、可枚举性来控制属性的操纵限制,对象也具有可扩展性来限制对象的一些行为。

当将对象的可扩展性设置为 false 时,就无法再动态的为对象添加属性。默认创建的新对象,都是具有可扩展性的。

不像属性的特性那样,还专门定义了一个属性描述符对象来控制属性的特性,对于对象的可扩展性,操作很简单:

object.isextensible()

使用 object.isextensible() 来获取对象的可扩展性描述,返回 true,表示对象是可扩展的,即可动态添加属性。

#### object.preventextensions()

同样,可使用 object.preventextensions() 来设置对象的不可扩展,参数传入对象即可。这样,这个对象就不可动态添加属性了。

但有几点需要注意:

  • 一旦将对象设置为不可扩展,就无法再将其转换回可扩展了
  • 可扩展性只限制于对象本身,对对象的原型并不影响,在原型上添加的属性仍可动态同步到子对象上

针对于对象的可扩展性,对象属性的可写性、可配置性、可枚举性这些操作,object 内封装了一些便捷的方法,如:

  • object.seal():将对象设置为不可扩展,同时,将对象所有自有属性都设置为不可配置,通常称封闭对象。可用 object.issealed() 来检测对象是否被封闭。
  • object.freeze():将对象设置为不可扩展,同时,将对象所有自有属性不可配置且只读,通常称冻结对象。可用 object.isfrozen() 来检测对象是否被冻结。

大家好,我是 dasu,欢迎关注我的公众号(dasuandroidtv),公众号中有我的联系方式,欢迎有事没事来唠嗑一下,如果你觉得本篇内容有帮助到你,可以转载但记得要关注,要标明原文哦,谢谢支持~
前端入门10-JavaScript语法之对象