ECMA-262 将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。正因为如此(以及其他还未讨论的原因),可以把 ECMAScript 的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数。

1 理解对象

创建自定义对象的通常方式是创建 Object 的一个新实例,然后再给它添加属性和方法,如下例所示:

let person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function() { console.log(this.name);
};

这个例子创建了一个名为 person 的对象,而且有三个属性(name、age 和 job)和一个方法(sayName())。sayName()方法会显示 this.name 的值,这个属性会解析为 person.name。早期JavaScript 开发者频繁使用这种方式创建新对象。几年后,对象字面量变成了更流行的方式。前面的例子如果使用对象字面量则可以这样写:

let person = { name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); }
};

这个例子中的 person 对象跟前面例子中的 person 对象是等价的。

1.1 属性的类型

ECMA-262 使用一些内部特性来描述属性的特征。这些特性是由为 JavaScript 实现引擎的规范定义的。因此,开发者不能在 JavaScript 中直接访问这些特性。为了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来,比如[[Enumerable]]。

属性分两种:数据属性和访问器属性。

1.1.1 数据属性

数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有 4 个特性描述它们的行为。

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
  • [[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是 true,如前面的例子所示。
  • [[Value]]:包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为 undefined。

在像前面例子中那样将属性显式添加到对象之后,[[Configurable]]、[[Enumerable]]和[[Writable]]都会被设置为 true,而[[Value]]特性会被设置为指定的值。比如:

let person = { name: "Nicholas"
};

这里,我们创建了一个名为 name 的属性,并给它赋予了一个值"Nicholas"。这意味着[[Value]]特性会被设置为"Nicholas",之后对这个值的任何修改都会保存这个位置。

要修改属性的默认特性,就必须使用 Object.defineProperty()方法。这个方法接收 3 个参数:要给其添加属性的对象、属性的名称和一个描述符对象。最后一个参数,即描述符对象上的属性可以包含:configurable、enumerable、writable 和 value,跟相关特性的名称一一对应。根据要修改的特性,可以设置其中一个或多个值。比如:

let person = {};
Object.defineProperty(person, "name", { writable: false, value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas"

这个例子创建了一个名为 name 的属性并给它赋予了一个只读的值"Nicholas"。这个属性的值就不能再修改了,在非严格模式下尝试给这个属性重新赋值会被忽略。在严格模式下,尝试修改只读属性的值会抛出错误。

类似的规则也适用于创建不可配置的属性。比如:

let person = {};
Object.defineProperty(person, "name", { configurable: false, value: "Nicholas"
});
console.log(person.name); // "Nicholas"
delete person.name;
console.log(person.name); // "Nicholas"

这个例子把 configurable 设置为 false,意味着这个属性不能从对象上删除。非严格模式下对这个属性调用 delete 没有效果,严格模式下会抛出错误。此外,一个属性被定义为不可配置之后,就不能再变回可配置的了。再次调用 Object.defineProperty()并修改任何非 writable 属性会导致错误:

let person = {};
Object.defineProperty(person, "name", { configurable: false, value: "Nicholas"
});
// 抛出错误
Object.defineProperty(person, "name", { configurable: true, value: "Nicholas"
});

因此,虽然可以对同一个属性多次调用 Object.defineProperty(),但在把 configurable 设置为 false 之后就会受限制了。

在调用 Object.defineProperty()时,configurable、enumerable 和 writable 的值如果不指定,则都默认为 false。多数情况下,可能都不需要 Object.defineProperty()提供的这些强大的设置,但要理解 JavaScript 对象,就要理解这些概念。

1.1.2 访问器属性

访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。访问器属性有 4 个特性描述它们的行为。

  • [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  • [[Get]]:获取函数,在读取属性时调用。默认值为 undefined。
  • [[Set]]:设置函数,在写入属性时调用。默认值为 undefined。

访问器属性是不能直接定义的,必须使用 Object.defineProperty()。下面是一个例子:

// 定义一个对象,包含伪私有成员 year_和公共成员 edition
let book = { year_: 2017, edition: 1};
Object.defineProperty(book, "year", { get() { return this.year_; }, set(newValue) { if (newValue > 2017) { this.year_ = newValue; this.edition += newValue - 2017; } }
});
book.year = 2018;
console.log(book.edition); // 2

待补充 233

2 创建对象

虽然使用 Object 构造函数或对象字面量可以方便地创建对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。

2.1 概述

综观 ECMAScript 规范的历次发布,每个版本的特性似乎都出人意料。ECMAScript 5.1 并没有正式支持面向对象的结构,比如类或继承。但是,正如接下来几节会介绍的,巧妙地运用原型式继承可以成功地模拟同样的行为。

ECMAScript 6 开始正式支持类和继承。ES6 的类旨在完全涵盖之前规范设计的基于原型的继承模式。不过,无论从哪方面看,ES6 的类都仅仅是封装了 ES5.1 构造函数加原型继承的语法糖而已。

但不要误会:采用面向对象编程模式的 JavaScript 代码还是应该使用 ECMAScript 6 的类。但不管怎么说,理解 ES6 类出现之前的惯例总是有益无害的。特别是 ES6 的类定义本身就相当于对原有结构的封装。因此,在介绍 ES6 的类之前,本书会循序渐进地介绍被类取代的那些底层概念。

2.2 工厂模式

工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。下面的例子展示了一种按照特定接口创建对象的方式:

function createPerson(name, age, job) { let o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function() { console.log(this.name); }; return o;
}
let person1 = createPerson("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");

这里,函数 createPerson()接收 3 个参数,根据这几个参数构建了一个包含 Person 信息的对象。可以用不同的参数多次调用这个函数,每次都会返回包含 3 个属性和 1 个方法的对象。这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)。

2.3 构造函数模式

前面几章提到过,ECMAScript 中的构造函数是用于创建特定类型对象的。像 Object 和 Array 这样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。

比如,前面的例子使用构造函数模式可以这样写:

function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = function() { console.log(this.name); };
} let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");person1.sayName(); // Nicholas
person2.sayName(); // Greg

在这个例子中,Person()构造函数代替了 createPerson()工厂函数。实际上,Person()内部的代码跟 createPerson()基本是一样的,只是有如下区别。

  • 没有显式地创建对象
  • 属性和方法直接赋值给了this
  • 没有return

另外,要注意函数名 Person 的首字母大写了。按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。这是从面向对象编程语言那里借鉴的,有助于在 ECMAScript 中区分构造函数和普通函数。毕竟 ECMAScript 的构造函数就是能创建对象的函数。

要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。

  1. 在内存中创建一个新对象
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

上一个例子的最后,person1 和 person2 分别保存着 Person 的不同实例。这两个对象都有一个constructor 属性指向 Person,如下所示:

console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true

constructor 本来是用于标识对象类型的。不过,一般认为 instanceof 操作符是确定对象类型更可靠的方式。前面例子中的每个对象都是 Object 的实例,同时也是 Person 的实例,如下面调用instanceof 操作符的结果所示:

console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true

定义自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处。在这个例子中,person1 和 person2 之所以也被认为是 Object 的实例,是因为所有自定义对象都继承自 Object(后面再详细讨论这一点)。

构造函数不一定要写成函数声明的形式。赋值给变量的函数表达式也可以表示构造函数:

let Person = function(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { console.log(this.name); };
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true

在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。只要有 new 操作符,就可以调用相应的构造函数:

function Person() { this.name = "Jake"; this.sayName = function() { console.log(this.name); };
}
let person1 = new Person();
let person2 = new Person;
person1.sayName(); // Jake
person2.sayName(); // Jake
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
2.3.1 构造函数也是函数

待补充 248

2.4 原型模式

2.5 对象迭代

3 继承

继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承:接口继承和实现继承。前者只继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签名。实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。

3.1 原型链

ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。

待补充 263

8 对象、类与面向对象编程相关推荐

  1. 第八章:对象、类与面向对象编程

    第八章:对象.类与面向对象编程 8.1 理解对象 new一个对象的时候发生了什么 8.1.1 属性的类型 通过两个中括号访问内部特性 1. 数据属性 数据属性包含一个保存数据值的位置 有 4 个特性描 ...

  2. 《易学Python》——第6章 类与面向对象编程 6.1 类是什么

    本节书摘来自异步社区<易学Python>一书中的第6章,第6.1节,作者[澳]Anthony Briggs,王威,袁国忠 译,更多章节内容可以访问云栖社区"异步社区"公 ...

  3. Python类及面向对象编程【转】

    Python类及面向对象编程 类是用来创建数据结构和新类型对象的主要机制.本章的主题就是类,面向对象编程和设计不是本章的重点.本章假定你具有数据结构的背景知识及一定的面向对象的编程经验(其它面向对象的 ...

  4. 列表怎么有限的初始化为零_《零基础学习Android开发》第五课 类与面向对象编程1-1...

    视频:<零基础学习Android开发>第五课 类与面向对象编程1-1 类的定义.成员变量.构造方法.成员方法 一.从数据与逻辑相互关系审视代码 通过前面的课程,我们不断接触Java语言的知 ...

  5. python面向对象编程72讲_2020-07-22 Python学习笔记27类和面向对象编程

    一些关于自己学习Python的经历的内容,遇到的问题和思考等,方便以后查询和复习. 声明:本人学习是在扇贝编程通过网络学习的,相关的知识.案例来源于扇贝编程.如果使用请说明来源. 第27关 类与面向对 ...

  6. python 类和对象_Python零基础入门学习33:类与面向对象编程:类的继承

    注:本文所有代码均经过Python 3.7实际运行检验,保证其严谨性. 本文字数约1300,阅读时间约为3分钟. Python面向对象编程 类的继承机制 如果一个类A继承自另一个类B,就把继承者类A称 ...

  7. python类和对象基础_Python(基础)---类和面向对象编程

    一.类的基本概念 1.1 什么叫类 python是一门高级语言,与汇编不同,它的语法规则更贴近于我们的现实生活. 而类就是对现实生活中实际事物的抽象,例如:汽车,人,动物等抽象概念,这些抽象出来的东西 ...

  8. python的面向对象编程学生成绩_python的类_面向对象编程

    摘自谬雪峰https://www.liaoxuefeng.com/wiki/1016959663602400/1017496031185408 面向对象编程(定义对象)和面向过程(定义函数)的区别,各 ...

  9. ad09只在一定范围内查找相似对象_23、面向对象编程

    目录: 对象的概念 类与对象 面向对象编程 类的定义与实例化 属性访问 类属性与对象属性 属性查找顺序与绑定方法 小结 视频链接 一 对象的概念 "面向对象"的核心是"对 ...

  10. 5. 设计模式之对象思维:面向对象编程有哪些优势?

    一.编程语言 VS 编程范式 现在我们一说到"面向对象编程"似乎感觉就是编程的全部,实际上它是 20 世纪 60 年代就已经出现的一门"古老"技术,在 2000 ...

最新文章

  1. IPC之哲学家进餐问题
  2. 局域网读取文件_教你windows局域网如何设置共享文件
  3. 程序控制发送文件到邮箱_Kindle电子邮箱推送
  4. 工作流实战_14_flowable_已办任务列表查询
  5. 第1章—Spring之旅—简化Spring的java开发
  6. 张小龙:微信产品观(上)
  7. Vue3 Mixin的使用方法(全局,局部,setup内部使用)
  8. WIN10网络打印机-打印失败解决方案
  9. cobol学习4--语法与文法(2)
  10. HTML、CSS、JavaScript学习总结
  11. KubeVela 1.3 发布:开箱即用的可视化应用交付平台,引入插件生态、权限认证、版本化等企业级新特性
  12. 全球与中国苯乙烯-异戊二烯嵌段共聚物市场现状及未来发展趋势
  13. VBA破解Excel表格保护密码
  14. 计算机学院请假管理系统,《计算机学院在校学生离校请销假管理暂行规定》
  15. 孙振耀--感悟工作与生活
  16. Adobe Acrobat更改注释的作者姓名
  17. python找出字符串中的最长回文串子序列
  18. 【空间分析】地理探测器法原理及应用
  19. SKU和SPU什么意思,到底有什么关联(电商)
  20. 杭州大学计算机学院博士后,不用去深圳,博士后年薪30万,在杭州这所高校也能轻松达到!...

热门文章

  1. FFmpeg入门知识(二):Windows环境下编译FFMPEG源码
  2. 函数 strncpy、strncat、strncmp、strrchr 的实现
  3. this的作用(转)
  4. 黄淮学院计算机专业录取分数线2019,黄淮学院2020年录取分数线(附2017-2020年分数线)...
  5. 乐高积木格斗机器人组装拼图_玩积木、组装机器人的多重好处,你一定想不到!(认识机器人)...
  6. Apache Flink 的迁移之路,2 年处理效果提升 5 倍
  7. 直播技术总结(二)ijkplayer的编译到Android平台并测试解码库
  8. java autointeger_【Java多线程】线程安全的Integer,AutomicInteger
  9. js layui 弹出子窗体_layui 弹出界面弹框
  10. 微信小程序多次跳转后不能点_京东小程序 Taro 开发对比原生开发测评