Loading... ## 对象 **概述:** 在JavaScript中的定义是:几乎“所有事物”都是对象,对象是相关属性和方法的集合体。 ### 创建对象 #### 创建内置对象 在JavaScript中我们可以使用 `new`关键字创建对象,例如创建一个Date对象:`var date = new Date();`、创建一个数组:`var arr = new Array();`,当然对于 `Math`对象所有属性和方法都是静态的,我们不需要使用 `new`关键字创建它,而是直接通过 `Math`关键字使用。 #### 创建自定义对象 **语法:** ```javascript var 对象名 = new Object(); ``` **演示案例:** ```javascript /** * 在下方案例中,我们将创建对象并为其添加属性和方法 * 创建person对象 */ var person = new Object(); //给person对象添加属性 person.name = '张三'; person.age = 18; person.sex = '男生'; person.birthday = '2004-10-1'; //给person对象添加方法 person.sayHi = function () { console.log(`大家好,我是:${this.name},今年:${this.age}岁,我是${this.sex}生,我的出生日期是:${this.birthday}`) } //调用方法 person.sayHi(); //输出: 大家好,我是:张三,今年:18岁,我是男生,我的出生日期是:2004-10-1 ``` <div class="tip inlineBlock warning simple"> 在 `sayHi`方法中,我们使用 `this.属性名`用于获取person对应的属性,`this`是指当前person对象。 </div> **通过字面量创建对象【推荐】:** ```javascript /** * 在下方案例中,我们将创建对象并为其添加属性和方法 * 通过字面量赋值创建person对象 */ var person = { name: '张三', age: 18, sex: '男生', birthday: '2004-10-1', sayHi: function () { console.log(`大家好,我是:${this.name},今年:${this.age}岁,我是${this.sex}生,我的出生日期是:${this.birthday}`) } } //调用方法 person.sayHi(); //输出: 大家好,我是:张三,今年:18岁,我是男生生,我的出生日期是:2004-10-1 ``` ### 简单工厂模式 在上述案例中,无论是基于Object对象的方式还是使用字面量的方式创建对象,他们都有一个明显的缺陷:如果我们需要创建多个对象时,就需要产生大量的重复代码。 🤔如何解决这个问题呢? 很多开发者在软件开发过程中通常会将**设计模式**运用到项目中来。<span style='color:#1E90FF'> 所谓的模式是指在某一个环境下对于某个问题的一种解决方案,在软件开发中,有许多优秀的模式被保留下来供后续人员学习使用,我们就称其为:设计模式。</span>常见的设计模式有:工厂模式、单例模式、观察者模式、抽象工厂模式等等… **工厂模式**是最常用的一种用来创建对象的设计模式。通过封装不会暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数我们就可以称之为:工厂函数。工厂模式更具抽象程度可以分为以下三种: 1. 简单工厂模式 2. 工厂方法模式 3. 抽象工厂模式 我们也将使用**简单工厂模式**来解决上面的问题。 **演示案例:** ```javascript /** * 创建一个工厂函数,用于创建person对象 * @param {*} name 姓名 * @param {number} age 年龄 * @param {*} sex 性别 * @param {*} birthday 出生日期 * @returns person对象 */ function createPerson(name, age, sex, birthday) { var person = { name: name, age: age, sex: sex, birthday: birthday, sayHi: function () { console.log(`大家好,我是:${this.name},今年:${this.age}岁,我是${this.sex}生,我的出生日期是:${this.birthday}`) } } return person; } //调用工厂函数获取不同的对象 var lisi = createPerson("李四", 16, "男", "2006-1-18"); var wangwu = createPerson("王五", 20, "男", "2002-2-10"); //调用person对象中的sayHi方法 lisi.sayHi(); wangwu.sayHi(); /** * 依次输出: * 大家好,我是:李四,今年:16岁,我是男生,我的出生日期是:2006-1-18 * 大家好,我是:王五,今年:20岁,我是男生,我的出生日期是:2002-2-10 */ ``` <div class="tip inlineBlock warning simple"> **📋笔记:** 在上述案例中,`createPerson()`就是一个工厂,我们可以重复多次的调用这个工程函数用于创建多个的对象,从而避免了大量的重复代码。 **📝弊端:** 使用简单工厂模式创建对象虽然解决了创建多个对象时需要大量的重复代码的问题,但是也有一个新的问题也随之出现即:对象识别困难(无法知道创建出来的对象是什么,在上述案例中无法体现)。在实际开发中可以使用**构造函数**来解决此问题。 </div> ## 构造函数和原型对象 ### 构造函数 **概述:** 构造函数用来创建特定的对象。在实际开发中,我们可以创建自定义的构造函数,从而可以自定义对象类型的属性和方法。**构造函数和普通函数一致**,只是当我们使用 `new`关键字的时候,它就能创建一个新的对象。在构造函数中所使用的 `this`关键字,代表的就是被创建的对象。 **案例演示:** ```javascript /** * 构造函数,用于创建Person对象 * @param {*} name 姓名 * @param {number} age 年龄 * @param {*} sex 性别 * @param {*} birthday 出生日期 * @returns person对象 */ function Person(name, age, sex, birthday) { //首字母大写,以区别于其他函数 this.name = name; this.age = age; this.sex = sex; this.birthday = birthday; this.sayHi = function () { console.log(`大家好,我是:${this.name},今年:${this.age}岁,我是${this.sex}生,我的出生日期是:${this.birthday}`) } } //使用new关键字调用构造函数获得新的对象 var lisi = new Person("李四", 16, "男", "2006-1-18"); var wangwu = new Person("王五", 20, "男", "2002-2-10"); //调用方法 lisi.sayHi(); wangwu.sayHi(); /** * 依次输出: * 大家好,我是:李四,今年:16岁,我是男生,我的出生日期是:2006-1-18 * 大家好,我是:王五,今年:20岁,我是男生,我的出生日期是:2002-2-10 */ ``` <div class="tip inlineBlock warning simple"> **⚠️注意:** 在创建构造函数的时候,需要将函数名称的首字母改为大写,采用:采用Pascal(帕斯卡)命名法,即所有单词首字母均需大写,这是为了能够和普通函数予以区分。 **💡构造函数与其他的创建对象的方式的不同:** 1. 没有显示的创建对象 2. 直接将属性和方法赋值给了this对象 3. 没有return语句 **🔦构造函数执行过程中会经历以下4个步骤:** 1. 创建一个新对象 2. 将构造函数的作用域赋给新对象,因此this就指向了这个新对象 3. 执行构造函数中的代码,为这个新对象添加属性及方法 【📌】 4. 返回新对象 </div> ### constructor属性和instanceof操作符 对象中包含 `constructor`属性,指向其构造函数。 使用 `instanceof`操作符检测对象类型。 **演示案例:** ```javascript /** * 构造函数,用于创建Person对象 * @param {*} name 姓名 * @param {number} age 年龄 * @param {*} sex 性别 * @param {*} birthday 出生日期 * @returns person对象 */ function Person(name, age, sex, birthday) { //首字母大写,以区别于其他函数 this.name = name; this.age = age; this.sex = sex; this.birthday = birthday; this.sayHi = function () { console.log(`大家好,我是:${this.name},今年:${this.age}岁,我是${this.sex}生,我的出生日期是:${this.birthday}`) } } /** * 创建学生构造函数 */ function Student() { this.name = ""; } //使用new关键字调用构造函数获得新的对象 var lisi = new Person("李四", 16, "男", "2006-1-18"); var wangwu = new Person("王五", 20, "男", "2002-2-10"); /** * 知识点: * ⭐️ 所有的对象都具备constructor属性,该属性指向当前对象的构造函数 * ⭐️ instanceof操作符用于检测对象所属类型 */ console.log(lisi.constructor == Person); //true console.log(lisi.constructor == Student); //false console.log(lisi instanceof Person); //true console.log(lisi instanceof Student); //false console.log(lisi instanceof Object); //true ``` ### 原型对象 在之前的案例中,我们可以使用构造函数来实例化不同的对象,我们也是通过:**执行构造函数中的代码,为这个新对象添加属性及方法。** 这也就意味着每当我们实例化一个新对象时,构造函数中的方法都将会被重新执行: ![案例演示说明](https://www.jbea.cn/usr/uploads/2022/09/2847467463.png) 我们可以优化上面的代码,通过函数定义的方式把 `sayHi()`方法转移到构造函数的外部,所有实例共享全局作用域中定义的同一个 `sayHello()`函数: ```javascript /** * 构造函数,用于创建Person对象 * @param {*} name 姓名 * @param {number} age 年龄 * @param {*} sex 性别 * @param {*} birthday 出生日期 * @returns person对象 */ function Person(name, age, sex, birthday) { //首字母大写,以区别于其他函数 this.name = name; this.age = age; this.sex = sex; this.birthday = birthday; this.sayHi = sayHello; //将构造函数中的sayHi指向全局sayHello函数,达到所有实例共享同一个函数的效果 } /** * 定义全局sayHello函数 */ function sayHello() { console.log(`大家好,我是:${this.name},今年:${this.age}岁,我是${this.sex}生,我的出生日期是:${this.birthday}`) } //使用new关键字调用构造函数获得新的对象 var lisi = new Person("李四", 16, "男", "2006-1-18"); var wangwu = new Person("王五", 20, "男", "2002-2-10"); //输出打印 lisi.sayHi(); //大家好,我是:李四,今年:16岁,我是男生,我的出生日期是:2006-1-18 wangwu.sayHi(); //大家好,我是:王五,今年:20岁,我是男生,我的出生日期是:2002-2-10 sayHello(); //大家好,我是:,今年:undefined岁,我是undefined生,我的出生日期是:undefined ``` <div class="tip inlineBlock error simple"> **弊端:** 我们将sayHello()修改成了全局函数,这也就意味着sayHello()函数可以直接被调用,但是当我们直接调用sayHello()时,系统所打印的数据全为:`undefined`,这也就发现了一个新的问题,sayHello()函数只能给某一个对象调用,这样的行为破坏了自定义类型的封装性。 </div> **🤔如何解决?** 无论何时,我们只要创建一个函数,就会按照特定的规则为这个函数创建一个 `prototype`(原型)属性,该属性指向原型对象。默认情况下,所有原型对象自动获得一个名为 `constructor`的属性,指回与之相关的构造函数。我们可以通过 `prototype`(原型)对象方式所添加的属性和方法在所有的实例中都能访问和使用,语法格式: ```javascript 构造函数名.prototype.新属性或新方法; ``` **演示案例:** ```javascript /** * 创建构造函数 * 声明构造函数 Student 后,函数就有了一个与之关联的原型对象 */ function Student() { console.log("Student被创建"); } /** * 给 Student 关联的prototype对象添加属性和方法 * 这些属性和方法为各个对象实例所共享 */ Student.prototype.name = "张三"; Student.prototype.age = 18; Student.prototype.email = "zhangsan@163.com"; Student.prototype.hobby = "打球"; /** * 通过 prototype 添加方法 */ Student.prototype.showName = function () { console.log(`大家好,我叫:${this.name}`) } /** * 调用 Student 构造函数,创建对象实例 * 在创建对象时,进入对应的构造函数 * 对象实例调用原型对象中共享的方法 */ var stu1 = new Student(); stu1.showName(); var stu2 = new Student(); stu2.showName(); var stu3 = new Student(); /** * 输出内容: * Student被创建 * 大家好,我叫:张三 * Student被创建 * 大家好,我叫:张三 * Student被创建 */ ``` ![原型链图](https://www.jbea.cn/usr/uploads/2022/09/1927065606.png) <div class="tip inlineBlock success simple"> **💻关系说明:** * 构造函数、原型对象和对象实例是3个完全不同的对象(在JavaScript中,函数也是对象)。 * 实例通过内部的隐藏特性 `[[Prototype]]`链接到原型对象(在Chrome、Firefox和Safari浏览器中可以通过 `__proto__`访问该特性),构造函数通过 `prototype`属性链接到原型对象。 * 实例与构造函数没有并没有直接联系而是与原型对象有直接联系,原型对象中的 `constructor`属性指向对应的构造函数。 * 同一个构造函数创建的多个实例,共享同一个原型对象。 * 正常的原型链会终止于Object的原型对象,Object原型的原型是null。 </div> **原型层级查找:** 实例对象访问属性时,会按照属性名开始逐层查找。 1. 优先查找实例对象本身。如果当前实例找到了对应的属性名,则返回对应的属性值。 2. 如果没有找到这个属性,则会沿着指针进入原型对象继续查找。在原型对象上找到属性后,再返回属性值。 ```javascript /** * 创建构造函数 * 声明构造函数 Student 后,函数就有了一个与之关联的原型对象 */ function Student() { this.job = "写代码"; } Student.prototype.name = "张三"; Student.prototype.age = 18; Student.prototype.email = "zhangsan@163.com"; Student.prototype.hobby = "打球"; Student.prototype.showName = function () { console.log(`大家好,我叫:${this.name}`) } var stu = new Student(); stu.className = "2020级理工4班"; console.log(stu.className, stu.name, stu.job); //2020级理工4班 张三 写代码 ``` **属性屏蔽:** 只要给对象实例添加一个属性,这个属性就会**遮蔽**原型对象上的同名属性。 ```javascript function Student() { } Student.prototype.name = "张三"; Student.prototype.age = 18; Student.prototype.email = "zhangsan@163.com"; Student.prototype.hobby = "打球"; Student.prototype.showName = function () { console.log(`大家好,我叫:${this.name}`) } var stu = new Student(); stu.name = "李四"; //覆盖原型属性name console.log(stu); // Student { name: '李四' } console.log(stu.__proto__); //{ name: '张三', age: 18, email: 'zhangsan@163.com', hobby: '打球', showName: ƒ, … } console.log(stu.name, stu.hobby); //李四 打球 ``` <div class="tip inlineBlock success simple"> **🙌经验分享:** 原型模式适合封装方法,构造函数模式适合封装属性,综合两种模式的优点就有了组合模式。 </div> 🛠**使用组合模式优化原始案例:** ```javascript /** * 构造函数,用于创建Person对象 * @param {*} name 姓名 * @param {number} age 年龄 * @param {*} sex 性别 * @param {*} birthday 出生日期 * @returns person对象 */ function Person(name, age, sex, birthday) { //首字母大写,以区别于其他函数 this.name = name; this.age = age; this.sex = sex; this.birthday = birthday; } /** * 使用 prototype 添加方法 */ Person.prototype.sayHi = function () { console.log(`大家好,我是:${this.name},今年:${this.age}岁,我是${this.sex}生,我的出生日期是:${this.birthday}`) } //使用new关键字调用构造函数获得新的对象 var lisi = new Person("李四", 16, "男", "2006-1-18"); var wangwu = new Person("王五", 20, "男", "2002-2-10"); //输出打印 lisi.sayHi(); //大家好,我是:李四,今年:16岁,我是男生,我的出生日期是:2006-1-18 wangwu.sayHi(); //大家好,我是:王五,今年:20岁,我是男生,我的出生日期是:2002-2-10 ``` **原型案例扩展:** 我们在使用日期对象时,获取的时间都需要手动格式化,我们今天给他扩展一个格式化的方法。 ```javascript /** * 给Date对象的原型添加一个格式化方法 * yyyy-MM-dd → 2022-10-01 * yy-MM-dd h:m:s → 22-10-01 11:5:21 * @param {*} fmt 年(y)可以用 1-4 个占位符、月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字) * @returns 格式化结果 */ Date.prototype.format = function (fmt) { var o = { "M+": this.getMonth() + 1, //月份 "d+": this.getDate(), //日 "h+": this.getHours(), //小时 "m+": this.getMinutes(), //分 "s+": this.getSeconds(), //秒 "q+": Math.floor((this.getMonth() + 3) / 3), //季度 "S": this.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); } for (var k in o) { if (new RegExp("(" + k + ")").test(fmt)) { fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); } } return fmt; } //如何使用 var d = new Date(); console.log(d); //Sun Sep 04 2022 11:08:10 GMT+0800 (中国标准时间) console.log(d.format('yyyy-MM-dd hh:mm:ss')); //2022-09-04 11:08:10 ``` ## 继承 **概述:**在面向对象编程中,继承是其中一大重要特性。通过继承可以减少大量的重复代码,也可以解决程序在后期维护和扩展上所遇到的问题。在其他的变成语言中,实现继承的主要方式有两种:实现类继承和接口继承。在是在ES6出现之前,JavaScript中并没有类和接口的概念,在ES6之前只能通过**原型链**来实现继承。【在之后的笔记中,我们会将ES6中的继承,本次笔记暂时跳过】。 ### 原型链 **概述:**在ECMAScript中将原型链做为实现继承的主要方法,它的基本思想是利用原型让一个应用类型继承另一个应用类型的属性和方法。在JavaScript中,每个构造函数拥有一个原型对象,原型对象包含一个指向构造函数的内部属性(`constructor`),实例都包含一个指向原型对象的内部属性(`__proto__`),假如一个原型对象等于另一个类型的实例,另一个类型的原型对象又等于另一个类型的实力,如此层层递进,就构成了实例与原型的链条,这就是原型链。 **演示案例:** ```javascript /** * 构造函数:Person */ function Person() { this.foot = 2; } /** * 使用 prototype 添加方法 * @returns 获取腿的个数 */ Person.prototype.getFoot = function () { return this.foot; } /** * 构造函数:Woman */ function Woman() { this.head = 1; } //使用原型对象继承 Woman.prototype = new Person(); /** * 使用 prototype 添加方法 * @returns 获取头的个数 */ Woman.prototype.getHead = function () { return this.head; } //创建对象 var woman1 = new Woman(); alert(woman1.getFoot()); //调用父类方法: 输出 2 alert(woman1 instanceof Woman); //true alert(woman1 instanceof Person); //true alert(woman1 instanceof Object); //true ``` ![原型链图](https://www.jbea.cn/usr/uploads/2022/09/1671535847.png) <div class="tip inlineBlock share simple"> **⚠️注意:** 📍`woman1`调用 `getFoot()`的步骤:搜索实例 → 搜索 `Woman.prototype` → 搜索 `Person.prototype`,搜索的过程要一环一环地前行到原型链的末端才会停下来。 📍所有函数的默认原型都是Object对象的实例。 </div> **方法重写** ```javascript /** * 构造函数:Person */ function Person() { this.foot = 2; } /** * 使用 prototype 添加方法 * @returns 获取腿的个数 */ Person.prototype.getFoot = function () { return this.foot; } /** * 构造函数:Woman */ function Woman() { this.head = 1; } //使用原型对象继承 Woman.prototype = new Person(); /** * 使用 prototype 添加方法 * @returns 获取头的个数 */ Woman.prototype.getHead = function () { return this.head; } /** * 重写父类方法: getFoot * @returns */ Woman.prototype.getFoot = function () { return false; } //创建对象 var woman1 = new Woman(); console.log(woman1.getFoot()); //调用方法: 输出 false ``` <div class="tip inlineBlock share simple"> **⚠️注意:** Woman的实例调用 `getFoot()`方法时,调用的是重写后的方法,但是通过Person的实例调用 `getFoot()`方法时,还会调用原来的方法 </div> ### 对象继承 **🤔代码分析,下面的代码会输出什么?** ```javascript /** * 构造函数:Person */ function Person() { this.money = 200; this.myCar = []; } Person.prototype.showMyAssets = function () { console.log(`我的余额还有:${this.money},我的名下有:${this.myCar.length}辆车,它们分别是:${this.myCar.join(',')}`); } /** * 构造函数:Woman */ function Woman() { } //使用原型对象继承 Woman.prototype = new Person(); //创建对象 var woman1 = new Woman(); woman1.money = 500; woman1.myCar.push("自行车", "保时捷"); var woman2 = new Woman(); woman2.money = 800; woman2.myCar.push("桑塔纳", "兰博基尼", "特斯拉"); woman1.showMyAssets(); woman2.showMyAssets(); ``` <div class="tip inlineBlock info simple"> **💻输出结果:** woman1:我的余额还有:500,我的名下有:5辆车,它们分别是:自行车,保时捷,桑塔纳,兰博基尼,特斯拉 woman2:我的余额还有:800,我的名下有:5辆车,它们分别是:自行车,保时捷,桑塔纳,兰博基尼,特斯拉 ❓ 两个实例输出的信息一样,为什么? Person构造函数中包含引用类型值的原型,由于包含引用类型值的原型属性会被所有实例共享 ,在通过原型实现继承时,原型实际上会变成另一个类型的实例,因此,原先的实例属性也就变成了现在的原型属性了。 ❓ 如果Person提供了参数,我们该如何向父类型的构造函数中传参呢? 使用原型链无法实现,更换新方案:**借用构造函数、组合继承** </div> #### 借用构造函数 **概述:** 借用构造函数的基本思想比较简单,实现方式是:在子类构造函数的内部调用父类的构造函数,可以通过 `apply()`方法或者 `call()`方法进行调用,其语法如下: **语法:** apply:`apply(thisObj,[arg1,arg2...]) //传递参数可选,参数必须以数组形式传递 ` call: `call(thisObj,arg1,arg2...) //传递参数可选,多个参数使用逗号隔开 ` **案例演示:** ```javascript /** * 构造函数:Person */ function Person() { this.money = 200; this.myCar = []; } /** * 构造函数:Woman */ function Woman() { Person.call(this); //Women继承了Person } //创建对象 var woman1 = new Woman(); woman1.money = 500; woman1.myCar.push("自行车", "保时捷"); var woman2 = new Woman(); woman2.money = 800; woman2.myCar.push("桑塔纳", "兰博基尼", "特斯拉"); console.log(woman1.myCar); // ['自行车', '保时捷'] console.log(woman2.myCar); // ['桑塔纳', '兰博基尼', '特斯拉'] ``` **案例扩展:** ```javascript /** * 构造函数:Person */ function Person() { this.money = 200; this.myCar = []; } Person.prototype.showMyAssets = function () { console.log(`我的余额还有:${this.money},我的名下有:${this.myCar.length}辆车,它们分别是:${this.myCar.join(',')}`); } /** * 构造函数:Woman */ function Woman() { Person.call(this); //Women继承了Person } //创建对象 var woman1 = new Woman(); woman1.money = 500; woman1.myCar.push("自行车", "保时捷"); var woman2 = new Woman(); woman2.money = 800; woman2.myCar.push("桑塔纳", "兰博基尼", "特斯拉"); //调用打印财产的方法 woman1.showMyAssets(); //error: woman1.showMyAssets is not a function 代码中断 woman2.showMyAssets(); ``` <div class="tip inlineBlock error simple"> 上述代码已经完成了继承,为什么在使用 `woman1.showMyAssets();`的时候提示错误? 💡**在使用 `apply()`或 `call()`实现继承时,只能继承属性!** </div> #### 组合继承 **案例演示:** ```javascript /** * 构造函数:Person */ function Person() { this.money = 200; this.myCar = []; } Person.prototype.showMyAssets = function () { console.log(`我的余额还有:${this.money},我的名下有:${this.myCar.length}辆车,它们分别是:${this.myCar.join(',')}`); } /** * 构造函数:Woman */ function Woman() { Person.call(this); //Women继承了Person中的属性 } Woman.prototype = new Person(); //Women继承了Person中的方法 //创建对象 var woman1 = new Woman(); woman1.money = 500; woman1.myCar.push("自行车", "保时捷"); var woman2 = new Woman(); woman2.money = 800; woman2.myCar.push("桑塔纳", "兰博基尼", "特斯拉"); //调用打印财产的方法 woman1.showMyAssets(); //我的余额还有:500,我的名下有:2辆车,它们分别是:自行车,保时捷 woman2.showMyAssets(); //我的余额还有:800,我的名下有:3辆车,它们分别是:桑塔纳,兰博基尼,特斯拉 ``` **⭐️案例扩展,实现参数的传递:** ```javascript /** * 构造函数:Person */ function Person(money) { this.money = money; this.myCar = []; } //定义方法,实现财产输出 Person.prototype.showMyAssets = function () { console.log(`我的余额还有:${this.money},我的名下有:${this.myCar.length}辆车,它们分别是:${this.myCar.join(',')}`); } /** * 构造函数:Woman */ function Woman(name, money) { Person.call(this, money); //Women继承了Person中的属性,并传递参数 this.name = name; } Woman.prototype = new Person(); //Women继承了Person中的方法 //定义方法,实现自我介绍和财产输出 Woman.prototype.showMyInfo = function () { console.log(`大家好,我叫:${this.name}`); this.showMyAssets(); //调用父类的方法 } //创建对象 var woman1 = new Woman('张三', 800); woman1.myCar.push("自行车", "保时捷"); var woman2 = new Woman('李四', 1200); woman2.myCar.push("桑塔纳", "兰博基尼", "特斯拉"); //调用自我介绍的方法 woman1.showMyInfo(); woman2.showMyInfo(); /** * 输出结果: * 大家好,我叫:张三 * 我的余额还有:800,我的名下有:2辆车,它们分别是:自行车,保时捷 * 大家好,我叫:李四 * 我的余额还有:1200,我的名下有:3辆车,它们分别是:桑塔纳,兰博基尼,特斯拉 */ ``` ## **💡 类 【重要扩展】** ### 基础概念 **概述:** JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。 ```javascript function Preson(age, name) { this.age = age; this.name = name; } Preson.prototype.sayHi = function () { console.log(`我叫:${this.name},今年:${this.age}岁`); }; var p = new Preson(16, '王五'); p.sayHi(); // 我叫:王五,今年:16岁 ``` 上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。 ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 `class`关键字,可以定义类。 基本上,ES6 的 `class`可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 `class`写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的 `class`改写,就是下面这样。 ```javascript class Preson { constructor(age, name) { this.age = age; this.name = name; } sayHi() { console.log(`我叫:${this.name},今年:${this.age}岁`); } } var p = new Preson(16, '王五'); p.sayHi(); // 我叫:王五,今年:16岁 ``` 上面代码定义了一个“类”,可以看到里面有一个 `constructor()`方法,这就是构造方法,而 `this`关键字则代表实例对象。这种新的 Class 写法,本质上与本章开头的 ES5 的构造函数 `Person`是一致的。 `Person`类除了构造方法,还定义了一个 `sayHi()`方法。注意,定义 `sayHi()`方法的时候,前面不需要加上 `function`这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加了会报错。 ES6 的类,完全可以看作构造函数的另一种写法。 ```javascript class Preson { // ... } console.log(typeof Preson); // "function" console.log(Preson === Preson.prototype.constructor); // true ``` 上面代码表明,类的数据类型就是函数,类本身就指向构造函数。 使用的时候,也是直接对类使用 `new`命令,跟构造函数的用法完全一致。 ```javascript class Bar { doStuff() { console.log('stuff'); } } const b = new Bar(); b.doStuff() // "stuff" ``` 构造函数的 `prototype`属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的 `prototype`属性上面。 ```javascript class Person { constructor() { // ... } sayHi() { // ... } doWork() { // ... } } // 等同于 Person.prototype = { constructor() { }, sayHi() { }, doWork() { }, }; ``` 上面代码中,`constructor()`、`sayHi()`、`doWork()`这三个方法,其实都是定义在 `Person.prototype`上面。 因此,在类的实例上面调用方法,其实就是调用原型上的方法。 ```javascript class B {} const b = new B(); b.constructor === B.prototype.constructor // true ``` 上面代码中,`b`是 `B`类的实例,它的 `constructor()`方法就是 `B`类原型的 `constructor()`方法。 ### 静态方法和静态属性: 在class中允许定义静态(static)方法和静态属性: ```javascript class Foo { static myStaticProp = 42; //定义静态属性 static classMethod() { //定义静态方法 return 'hello'; } } Foo.classMethod() // 'hello' console.log(Foo.myStaticProp) // 42 var foo = new Foo(); foo.classMethod() // TypeError: foo.classMethod is not a function ``` 上面代码中,`Foo`类的 `classMethod`方法前有 `static`关键字,表明该方法是一个静态方法,可以直接在 `Foo`类上调用(`Foo.classMethod()`),而不是在 `Foo`类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。 注意,如果静态方法包含 `this`关键字,这个 `this`指的是类,而不是实例。 ```javascript class Foo { static bar() { // 在静态方法中调用静态方法 this.baz(); } static baz() { // 静态方法 console.log('hello'); } baz() { // 实例方法 console.log('world'); } } Foo.bar() // hello ``` 上面代码中,静态方法 `bar`调用了 `this.baz`,这里的 `this`指的是 `Foo`类,而不是 `Foo`的实例,等同于调用 `Foo.baz`。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。 ### 私有属性: [ES2022](https://github.com/tc39/proposal-class-fields)正式为 `class`添加了私有属性,方法是在属性名之前使用 `#`表示。 ```javascript class IncreasingCounter { #count = 0; get value() { console.log('Getting the current value!'); return this.#count; } increment() { this.#count++; } } ``` 上面代码中,`#count`就是私有属性,只能在类的内部使用(`this.#count`)。如果在类的外部使用,就会报错。 ```javascript const counter = new IncreasingCounter(); counter.#count // 报错 counter.#count = 42 // 报错 ``` 上面示例中,在类的外部,读取或写入私有属性 `#count`,都会报错。 ### 继承 Class 可以通过 `extends`关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。 ```javascript class Person { } class Student extends Person { } ``` 上面示例中,`Person`是父类,`Student`是子类,它通过 `extends`关键字,继承了 `Person`类的所有属性和方法。 ```javascript class Person { constructor(name, age, sex) { this.name = name; this.age = age; this.sex = sex; } sayHi() { console.log(`大家好,我叫:${this.name},我是${this.sex}生,今年:${this.age}岁`); } myFoo() { console.log('这是父类中的方法'); } } class Student extends Person { constructor(name, age, sex, className) { super(name, age, sex); //调用父类中的构造函数 this.className = className; } sayHi() { console.log(`大家好,我叫:${this.name},我是${this.sex}生,今年:${this.age}岁,来自于:${this.className}班`); //super.myFoo(); //可以通过super调用父类的方法、属性 } } var per = new Person('李四', 18, '男'); per.sayHi(); // 大家好,我叫:李四,我是男生,今年:18岁 var stu = new Student("张三", 14, '男', '理工40'); stu.sayHi(); // 大家好,我叫:张三,我是男生,今年:14岁,来自于:理工40班 ``` 上面示例中,`constructor()`方法,出现了 `super`关键字。`super`允许子类调用父类的构造函数、方法、属性。 ES6 规定,子类必须在 `constructor()`方法中调用 `super()`,否则就会报错。这是因为子类自己的 `this`对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用 `super()`方法,子类就得不到自己的 `this`对象。 ```javascript class Person { /* ... */ } class Student extends Person { constructor() { } } let stu = new Student(); // ReferenceError ``` 上面代码中,`Student`继承了父类 `Person`,但是它的构造函数没有调用 `super()`,导致新建实例时报错。 --- <div class="tip inlineBlock info simple"> ▶️ 开始学习下一章: [JavaScript+Jquery+ES6入门到放弃之ECMAScript 6 基础](https://jbea.cn/archives/564.html) </div> 最后修改:2022 年 09 月 09 日 © 允许规范转载 赞 3 都滑到这里了,不点赞再走!?