书香隽秀 - 6.面向对象编程
type: channel
link: https://www.huangshuxiu.com/oop/
description: 6.面向对象编程
items: 严格模式
items: Object 对象的相关方法
items: 对象的继承
items: this 关键字
items: 实例对象与 new 命令

严格模式
type: item
date: 2022-12-12T16:25:08+08:00
description: 严格模式除了正常的运行模式，JavaScript 还有第二种运行模式：严格模式（strict mode）。顾名思义，这种模式采用更加严格的 JavaScript 语法。同样的代码，在正常模式和严格模式中，可能会有不一样的运行结果。一些在正常模式下可以运行的语句，在严格模式下将不能运行。设计目的早期的 JavaScript 语言有很多设计不合理的地方，但是为了兼容以前的代码，又不能改变老的语法，只能不断添加新的语法，引导程序员使用新语法。严格模式是从 ES5 进入标准的，主要目的有以下几个。明确禁止一些不合理、不严谨的语法，减少 JavaScript 语言的一些怪异行为。增加更多报错的场合，消除代码运行的一些不安全之处，保证代码运行的安全。提高编译器效率，增加运行速度。为未来新版本的 JavaScript 语法做好铺垫。总之，严格模式体现了 JavaScript 更合理、更安全、更严谨的发展方向。启用方法进入严格模式的标志，是一行字符串use strict。'use strict';老版本的引擎会把它当作一行普通字符串，加以忽略。新版本的引擎就会进入严格模式。严格模式可以用于整个脚本，也可以只用于单个函数。（1） 整个脚本文件use strict放在脚本文件的第一行，整个脚本都将以严格模式运行。如果这行语句不在第一行就无效，整个脚本会以正常模式运行。(严格地说，只要前面不是产生实际运行结果的语句，use strict可以不在第一行，比如直接跟在一个空的分号后面，或者跟在注释后面。) 上面代码中，一个网页文件依次有两段 JavaScript 代码。前一个 （2）单个函数use strict放在函数体的第一行，则整个函数以严格模式运行。function strict() { 'use strict'; return '这是严格模式'; } function strict2() { 'use strict'; function f() { return '这也是严格模式'; } return f(); } function notStrict() { return '这是正常模式'; }有时，需要把不同的脚本合并在一个文件里面。如果一个脚本是严格模式，另一个脚本不是，它们的合并就可能出错。严格模式的脚本在前，则合并后的脚本都是严格模式；如果正常模式的脚本在前，则合并后的脚本都是正常模式。这两种情况下，合并后的结果都是不正确的。这时可以考虑把整个脚本文件放在一个立即执行的匿名函数之中。(function () { 'use strict'; // some code here })();显式报错严格模式使得 JavaScript 的语法变得更严格，更多的操作会显式报错。其中有些操作，在正常模式下只会默默地失败，不会报错。只读属性不可写严格模式下，设置字符串的length属性，会报错。'use strict'; 'abc'.length = 5; // TypeError: Cannot assign to read only property 'length' of string 'abc'上面代码报错，因为length是只读属性，严格模式下不可写。正常模式下，改变length属性是无效的，但不会报错。严格模式下，对只读属性赋值，或者删除不可配置（non-configurable）属性都会报错。// 对只读属性赋值会报错 'use strict'; Object.defineProperty({}, 'a', { value: 37, writable: false }); obj.a = 123; // TypeError: Cannot assign to read only property 'a' of object #<Object> // 删除不可配置的属性会报错 'use strict'; var obj = Object.defineProperty({}, 'p', { value: 1, configurable: false }); delete obj.p // TypeError: Cannot delete property 'p' of #<Object>只设置了取值器的属性不可写严格模式下，对一个只有取值器（getter）、没有存值器（setter）的属性赋值，会报错。'use strict'; var obj = { get v() { return 1; } }; obj.v = 2; // Uncaught TypeError: Cannot set property v of #<Object> which has only a getter上面代码中，obj.v只有取值器，没有存值器，对它进行赋…

Object 对象的相关方法
type: item
date: 2022-12-12T15:41:36+08:00
description: Object 对象的相关方法JavaScript 在Object对象上面，提供了很多相关方法，处理面向对象编程的相关操作。本章介绍这些方法。Object.getPrototypeOf()Object.getPrototypeOf方法返回参数对象的原型。这是获取原型对象的标准方法。var F = function () {}; var f = new F(); Object.getPrototypeOf(f) === F.prototype // true上面代码中，实例对象f的原型是F.prototype。下面是几种特殊对象的原型。// 空对象的原型是 Object.prototype Object.getPrototypeOf({}) === Object.prototype // true // Object.prototype 的原型是 null Object.getPrototypeOf(Object.prototype) === null // true // 函数的原型是 Function.prototype function f() {} Object.getPrototypeOf(f) === Function.prototype // trueObject.setPrototypeOf()Object.setPrototypeOf方法为参数对象设置原型，返回该参数对象。它接受两个参数，第一个是现有对象，第二个是原型对象。var a = {}; var b = {x: 1}; Object.setPrototypeOf(a, b); Object.getPrototypeOf(a) === b // true a.x // 1上面代码中，Object.setPrototypeOf方法将对象a的原型，设置为对象b，因此a可以共享b的属性。new命令可以使用Object.setPrototypeOf方法模拟。var F = function () { this.foo = 'bar'; }; var f = new F(); // 等同于 var f = Object.setPrototypeOf({}, F.prototype); F.call(f);上面代码中，new命令新建实例对象，其实可以分成两步。第一步，将一个空对象的原型设为构造函数的prototype属性（上例是F.prototype）；第二步，将构造函数内部的this绑定这个空对象，然后执行构造函数，使得定义在this上面的方法和属性（上例是this.foo），都转移到这个空对象上。Object.create()生成实例对象的常用方法是，使用new命令让构造函数返回一个实例。但是很多时候，只能拿到一个实例对象，它可能根本不是由构建函数生成的，那么能不能从一个实例对象，生成另一个实例对象呢？JavaScript 提供了Object.create()方法，用来满足这种需求。该方法接受一个对象作为参数，然后以它为原型，返回一个实例对象。该实例完全继承原型对象的属性。// 原型对象 var A = { print: function () { console.log('hello'); } }; // 实例对象 var B = Object.create(A); Object.getPrototypeOf(B) === A // true B.print() // hello B.print === A.print // true上面代码中，Object.create()方法以A对象为原型，生成了B对象。B继承了A的所有属性和方法。实际上，Object.create()方法可以用下面的代码代替。if (typeof Object.create !== 'function') { Object.create = function (obj) { function F() {} F.prototype = obj; return new F(); }; }上面代码表明，Object.create()方法的实质是新建一个空的构造函数F，然后让F.prototype属性指向参数对象obj，最后返回一个F的实例，从而实现让该实例继承obj的属性。下面三种方式生成的新对象是等价的。var obj1 = Object.create({}); var obj2 = Object.create(Object.prototype); var obj3 = new Object();如果想要生成一个不继承任何属性（比如没有toString()和valueOf()方法）的对象，可以将Object.create()的参数设为null。var obj = Object.create(n…

对象的继承
type: item
date: 2022-12-12T14:25:19+08:00
description: 对象的继承面向对象编程很重要的一个方面，就是对象的继承。A 对象通过继承 B 对象，就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。大部分面向对象的编程语言，都是通过“类”（class）实现对象的继承。传统上，JavaScript 语言的继承不通过 class，而是通过“原型对象”（prototype）实现，本章介绍 JavaScript 的原型链继承。ES6 引入了 class 语法，基于 class 的继承不在这个教程介绍，请参阅《ES6 标准入门》一书的相关章节。原型对象概述构造函数的缺点JavaScript 通过构造函数生成新对象，因此构造函数可以视为对象的模板。实例对象的属性和方法，可以定义在构造函数内部。function Cat (name, color) { this.name = name; this.color = color; } var cat1 = new Cat('大毛', '白色'); cat1.name // '大毛' cat1.color // '白色'上面代码中，Cat函数是一个构造函数，函数内部定义了name属性和color属性，所有实例对象（上例是cat1）都会生成这两个属性，即这两个属性会定义在实例对象上面。通过构造函数为实例对象定义属性，虽然很方便，但是有一个缺点。同一个构造函数的多个实例之间，无法共享属性，从而造成对系统资源的浪费。function Cat(name, color) { this.name = name; this.color = color; this.meow = function () { console.log('喵喵'); }; } var cat1 = new Cat('大毛', '白色'); var cat2 = new Cat('二毛', '黑色'); cat1.meow === cat2.meow // false上面代码中，cat1和cat2是同一个构造函数的两个实例，它们都具有meow方法。由于meow方法是生成在每个实例对象上面，所以两个实例就生成了两次。也就是说，每新建一个实例，就会新建一个meow方法。这既没有必要，又浪费系统资源，因为所有meow方法都是同样的行为，完全应该共享。这个问题的解决方法，就是 JavaScript 的原型对象（prototype）。prototype 属性的作用JavaScript 继承机制的设计思想就是，原型对象的所有属性和方法，都能被实例对象共享。也就是说，如果属性和方法定义在原型上，那么所有实例对象就能共享，不仅节省了内存，还体现了实例对象之间的联系。下面，先看怎么为对象指定原型。JavaScript 规定，每个函数都有一个prototype属性，指向一个对象。function f() {} typeof f.prototype // "object"上面代码中，函数f默认具有prototype属性，指向一个对象。对于普通函数来说，该属性基本无用。但是，对于构造函数来说，生成实例的时候，该属性会自动成为实例对象的原型。function Animal(name) { this.name = name; } Animal.prototype.color = 'white'; var cat1 = new Animal('大毛'); var cat2 = new Animal('二毛'); cat1.color // 'white' cat2.color // 'white'上面代码中，构造函数Animal的prototype属性，就是实例对象cat1和cat2的原型对象。原型对象上添加一个color属性，结果，实例对象都共享了该属性。原型对象的属性不是实例对象自身的属性。只要修改原型对象，变动就立刻会体现在所有实例对象上。Animal.prototype.color = 'yellow'; cat1.color // "yellow" cat2.color // "yellow"上面代码中，原型对象的color属性的值变为yellow，两个实例对象的color属性立刻跟着变了。这是因为实例对象其实没有color属性，都是读取原型对象的color属性。也就是说，当实例对象本身没有某个属性或方法的时候，它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。如果实例对象自身就有某个属性或方法，它就不会再去原型对象寻找这个属性或方法。cat1.color = 'black'; cat1.color // 'black' cat2.color // 'yellow' Animal.prototype.color // 'yellow';上面代码中，实例对象cat1的color属性改为blac…

this 关键字
type: item
date: 2022-12-12T09:04:54+08:00
description: this 关键字涵义this关键字是一个非常重要的语法点。毫不夸张地说，不理解它的含义，大部分开发任务都无法完成。前一章已经提到，this可以用在构造函数之中，表示实例对象。除此之外，this还可以用在别的场合。但不管是什么场合，this都有一个共同点：它总是返回一个对象。简单说，this就是属性或方法“当前”所在的对象。this.property上面代码中，this就代表property属性当前所在的对象。下面是一个实际的例子。var person = { name: '张三', describe: function () { return '姓名：'+ this.name; } }; person.describe() // "姓名：张三"上面代码中，this.name表示name属性所在的那个对象。由于this.name是在describe方法中调用，而describe方法所在的当前对象是person，因此this指向person，this.name就是person.name。由于对象的属性可以赋给另一个对象，所以属性所在的当前对象是可变的，即this的指向是可变的。var A = { name: '张三', describe: function () { return '姓名：'+ this.name; } }; var B = { name: '李四' }; B.describe = A.describe; B.describe() // "姓名：李四"上面代码中，A.describe属性被赋给B，于是B.describe就表示describe方法所在的当前对象是B，所以this.name就指向B.name。稍稍重构这个例子，this的动态指向就能看得更清楚。function f() { return '姓名：'+ this.name; } var A = { name: '张三', describe: f }; var B = { name: '李四', describe: f }; A.describe() // "姓名：张三" B.describe() // "姓名：李四"上面代码中，函数f内部使用了this关键字，随着f所在的对象不同，this的指向也不同。只要函数被赋给另一个变量，this的指向就会变。var A = { name: '张三', describe: function () { return '姓名：'+ this.name; } }; var name = '李四'; var f = A.describe; f() // "姓名：李四"上面代码中，A.describe被赋值给变量f，内部的this就会指向f运行时所在的对象（本例是顶层对象）。再看一个网页编程的例子。 上面代码是一个文本输入框，每当用户输入一个值，就会调用onChange回调函数，验证这个值是否在指定范围。浏览器会向回调函数传入当前对象，因此this就代表传入当前对象（即文本框），然后就可以从this.value上面读到用户的输入值。总结一下，JavaScript 语言之中，一切皆对象，运行环境也是对象，所以函数都是在某个对象之中运行，this就是函数运行时所在的对象（环境）。这本来并不会让用户糊涂，但是 JavaScript 支持运行环境动态切换，也就是说，this的指向是动态的，没有办法事先确定到底指向哪个对象，这才是最让初学者感到困惑的地方。实质JavaScript 语言之所以有 this 的设计，跟内存里面的数据结构有关系。var obj = { foo: 5 };上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面，生成一个对象{ foo: 5 }，然后把这个对象的内存地址赋值给变量obj。也就是说，变量obj是一个地址（reference）。后面如果要读取obj.foo，引擎先从obj拿到内存地址，然后再从该地址读出原始的对象，返回它的foo属性。原始的对象以字典结构保存，每一个属性名都对应一个属性描述对象。举例来说，上面例子的foo属性，实际上是以下面的形式保存的。{ foo: { value: 5 writable: true enumerable: true configurable: true } }注意，foo属性的值保存在属性描述对象的value属性里面。这样的结构是很清晰的，问题在于属性的值可能是一个函数。var obj = { foo: function () {} };这时，引擎会将函数单独保存在内存中，然后再将函数的地址赋值给foo属性的value属性。{ foo: { value: 函数的地址 ... } }由于函数是一个单独的…

实例对象与 new 命令
type: item
date: 2022-12-09T14:08:34+08:00
description: 实例对象与 new 命令JavaScript 语言具有很强的面向对象编程能力，本章介绍 JavaScript 面向对象编程的基础知识。对象是什么面向对象编程（Object Oriented Programming，缩写为 OOP）是目前主流的编程范式。它将真实世界各种复杂的关系，抽象为一个个对象，然后由对象之间的分工与合作，完成对真实世界的模拟。每一个对象都是功能中心，具有明确分工，可以完成接受信息、处理数据、发出信息等任务。对象可以复用，通过继承机制还可以定制。因此，面向对象编程具有灵活、代码可复用、高度模块化等特点，容易维护和开发，比起由一系列函数或指令组成的传统的过程式编程（procedural programming），更适合多人合作的大型软件项目。那么，“对象”（object）到底是什么？我们从两个层次来理解。（1）对象是单个实物的抽象。一本书、一辆汽车、一个人都可以是对象，一个数据库、一张网页、一个远程服务器连接也可以是对象。当实物被抽象成对象，实物之间的关系就变成了对象之间的关系，从而就可以模拟现实情况，针对对象进行编程。（2）对象是一个容器，封装了属性（property）和方法（method）。属性是对象的状态，方法是对象的行为（完成某种任务）。比如，我们可以把动物抽象为animal对象，使用“属性”记录具体是哪一种动物，使用“方法”表示动物的某种行为（奔跑、捕猎、休息等等）。构造函数面向对象编程的第一步，就是要生成对象。前面说过，对象是单个实物的抽象。通常需要一个模板，表示某一类实物的共同特征，然后对象根据这个模板生成。典型的面向对象编程语言（比如 C++ 和 Java），都有“类”（class）这个概念。所谓“类”就是对象的模板，对象就是“类”的实例。但是，JavaScript 语言的对象体系，不是基于“类”的，而是基于构造函数（constructor）和原型链（prototype）。JavaScript 语言使用构造函数（constructor）作为对象的模板。所谓”构造函数”，就是专门用来生成实例对象的函数。它就是对象的模板，描述实例对象的基本结构。一个构造函数，可以生成多个实例对象，这些实例对象都有相同的结构。构造函数就是一个普通的函数，但具有自己的特征和用法。var Vehicle = function () { this.price = 1000; };上面代码中，Vehicle就是构造函数。为了与普通函数区别，构造函数名字的第一个字母通常大写。构造函数的特点有两个。函数体内部使用了this关键字，代表了所要生成的对象实例。生成对象的时候，必须使用new命令。下面先介绍new命令。new 命令基本用法new命令的作用，就是执行构造函数，返回一个实例对象。var Vehicle = function () { this.price = 1000; }; var v = new Vehicle(); v.price // 1000上面代码通过new命令，让构造函数Vehicle生成一个实例对象，保存在变量v中。这个新生成的实例对象，从构造函数Vehicle得到了price属性。new命令执行时，构造函数内部的this，就代表了新生成的实例对象，this.price表示实例对象有一个price属性，值是1000。使用new命令时，根据需要，构造函数也可以接受参数。var Vehicle = function (p) { this.price = p; }; var v = new Vehicle(500);new命令本身就可以执行构造函数，所以后面的构造函数可以带括号，也可以不带括号。下面两行代码是等价的，但是为了表示这里是函数调用，推荐使用括号。// 推荐的写法 var v = new Vehicle(); // 不推荐的写法 var v = new Vehicle;一个很自然的问题是，如果忘了使用new命令，直接调用构造函数会发生什么事？这种情况下，构造函数就变成了普通函数，并不会生成实例对象。而且由于后面会说到的原因，this这时代表全局对象，将造成一些意想不到的结果。var Vehicle = function (){ this.price = 1000; }; var v = Vehicle(); v // undefined price // 1000上面代码中，调用Vehicle构造函数时，忘了加上new命令。结果，变量v变成了undefined，而price属性变成了全局变量。因此，应该非常小心，避免不使用new命令、直接调用构造函数。为了保证构造函数必须与new命令一起使用，一个解决办法是，构造函数内部使用严格模式，即第一行加上use strict。这样的话，一旦忘了使用new命令，直…