zoukankan      html  css  js  c++  java
  • 前端入门10-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";
    

    添加属性

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

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

    查询属性

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

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

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

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

    查询属性

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

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

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

    查询不存在的属性

    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,用这种方式就可以很好的判断对象是否含有某个属性。

    检测属性

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

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

    hasOwnProperty()

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

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

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

    检测属性

    首先利用上小节介绍的 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);    
    }
    

    看看输出的结果:

    遍历属性

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

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

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

    Object.keys()

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

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

    Object.keys()

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

    Object.getOwnPropertyNames()

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

    Object.getOwnPropertyNames

    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 的一个方法,接收两个参数,第一个参数是对象,第二个参数是对象内的某个自有属性,将会返回一个属性描述符对象:

    Object.getOwnPropertyDescriptor

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

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

    Object.getOwnPropertyDescriptors()

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

    Object.getOwnPropertyDescriptors

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

    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() 看下修改后的配置:

    Object.defineProperty

    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() 看一下这些属性的描述:

    存取器属性

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

    存取器属性.png

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

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

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

    对象的特性

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

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

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

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

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

    类属性

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

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

    类属性

    可扩展性

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

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

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

    Object.isExtensible()

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

    Object.preventExtensions()

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

    但有几点需要注意:

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

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

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

    大家好,我是 dasu,欢迎关注我的公众号(dasuAndroidTv),公众号中有我的联系方式,欢迎有事没事来唠嗑一下,如果你觉得本篇内容有帮助到你,可以转载但记得要关注,要标明原文哦,谢谢支持~
    dasuAndroidTv2.png

  • 相关阅读:
    $python日期和时间的处理
    $python生成器
    $思维导图——numpy基本知识
    $python用装饰器实现一个计时器
    $ MySQL-python数据库模块用法
    CentOS 7.2下编译安装PHP7.0.10+MySQL5.7.14+Nginx1.10.1
    Nginx、Apache工作原理及Nginx为何比Apache高效
    Apache的三种工作模式及相关配置
    ThinkPHP框架
    session与cookie的区别是什么?如果客户端禁用了cookie功能,将会对session有什么影响?
  • 原文地址:https://www.cnblogs.com/dasusu/p/10056824.html
Copyright © 2011-2022 走看看