前言
- 原型、原型链、类与继承似乎无时无刻的出现在我们身边,无论你是在面试中亦或是平常学习和工作中都有它的身影。那么这个是又是什么东西呢 ? 我曾通过 avaScript高级程序设计、你不知道的JavaScript、MDN文档以及教学视频。但似乎仍是半知半解,但我依然相信能通过这篇文章能让大家以及我搞懂这玩意到底是什么神仙。
大厂面试题分享 面试题库
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
什么是对象?
- 在ECMAScript中,对象是一组属性无序的集合,对象的每个属性或方法都有一个名称来标识,这个名称映射到一个值,你可以将其想像成一张散列表。
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存储存位置的数据结构。也就是说,它通过计算出一个键值的函数,将所需查询的数据映射到表中一个位置来让人访问,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
创建对象
- 创建自定义对象
// 创建一个Object的新实例,然后在实例上面添加属性和方法const person = newObject(); person.name = "moment"; person.age = 18; person.running = function () { console.log("我会游泳哦,你会吗"); }; 复制代码
- 对象字面量
const person = { running: function () { console.log("你会我也会啊"); }, name: "moment", age: 7, dance: function () { console.log("我还会跳舞呢"); }, }; 复制代码
- 两个实例中,每创建一个对象,都要为其创建一个name,age的属性以及running的方法,那么有没有什么方法可以让我们可以减少一些重复的代码呢,答案是有的。我们可以把方法抽取出来定义成一个函数,再给对象赋值。但是......
construnning = (info) => { console.log(info); }; const person = newObject(); person.name = "moment"; person.age = 18; person.running = running; const object = { running, name: "moment", age: 7, dance: function () { console.log("我还会跳舞呢"); }, }; // ,似乎只能封装公共的方法,属性无法动态传值,只能是固定的一个值 person.running("我们大家都会游泳哦"); // 我们大家都会游泳哦 object.running("我们大家都会游泳哦"); // 我们大家都会游泳哦复制代码
工厂函数
抽象工厂模式(英语:Abstract factory pattern)是一种软件开发设计模式。抽象工厂模式提供了一种方式,可以将一组具有同一主题的单独的工厂封装起来。在正常使用中,客户端程序需要创建抽象工厂的具体实现,然后使用抽象工厂作为接口来创建这一主题的具体对象。客户端程序不需要知道(或关心)它从这些内部的工厂方法中获得对象的具体类型,因为客户端程序仅使用这些对象的通用接口。抽象工厂模式将一组对象的实现细节与他们的一般使用分离开来。
- 按我个人的理解,工厂模式就像是一个模具,你能根据这个模具去生产产品,你只能改变其外观颜色,但是你不能改变其形状和大小。
functioncreateObject(name, age, info) { const obj = newObject(); obj.name = name; obj.age = age; obj.running = function () { console.log(info); }; return obj; } const person = createObject("moment", 18, "我会跑步"); const student = createObject("supper", 16, "我会跑步,我比你还年轻呢"); person.running(); // 我会跑步 student.running(); // 我会跑步,我比你还年轻呢复制代码
- 通过工厂模式,我们可以快速创建大量相似对象,没有重复代码。迎面走来走来的又是一个新问题,工厂模式创建的对象属于Object,无法区分对象类型,这也是工厂模式没有广泛使用的原因。
构造函数模式
- 没有显式地创建对象
- 属性和方法直接赋值给了this
- 没有return
functionPerson(name, age, info) { this.name = name; this.age = age; this.running = function () { console.log(info); }; } const person = newPerson("moment", 18, "我会跑步"); const student = newPerson("supper", 16, "我会跑步,我比你还年轻呢"); person.running(); // 我会跑步 student.running(); // 我会跑步,我比你还年轻呢复制代码
- 以上代码实际上执行的是这样的操作
person.name = "moment"; person.age = 18; person.running = function () { console.log("我会跑步"); }; student.name = "moment"; student.age = 16; student.running = function () { console.log("我会跑步,我比你还年轻呢"); }; 复制代码
- 要创建Person实例,应使用new操作符。
- 在内存创建一个新对象。
- 这个新对象内部的[[Prototype]] (proto)特性被赋值为构造函数的prototype属性, 即 person.proto=Person.prototype。
- 构造函数内部的this被赋值为这个新对象(即this指向新对象)。
- 执行构造函数内部的代码. 如 person.proto.name='moment'。
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
- new的过程可以参考以下代码
functionmyNew(func, ...args) { // 判断方法体if (typeof func !== "function") { throw"第一个参数必须是方法体"; } // 创建新对象// 这个对象的[[prototype]](隐式原型 __proto__)指向 func 这个类的原型对象 prototype// 即实例可以访问构造函数原型 obj.constructor === Personconst object = Object.create(func.prototype); // 构造函数内部的this被赋值为这个新对象const result = func.apply(object, args); // 如果构造函数返回的结果是引用数据类型,则返回运行后的结果// 否则返回新创建的 objconst isObject = typeof result === "object" && result !== null; const isFunction = typeof result === "function"; return isObject || isFunction ? result : object; } 复制代码
- 通过控制台打印不难发现,前面两个对象都是Object的实例,同时也是Person的实例
// instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上// person和student的隐式原型都指向Person的显式原型;console.log(person.__proto__ === student.__proto__); //trueconsole.log(person instanceofObject); //trueconsole.log(person instanceofPerson); //trueconsole.log(student instanceofObject); //trueconsole.log(student instanceofPerson); //true// 以上代码的代码实际上执行的该例子console.log(student.__proto__.__proto__.constructor.prototype === Object.prototype); console.log(student.__proto__.constructor.prototype === Person.prototype); 复制代码
- 构造方法虽然有用,但是也存在问题,虽然person和student都有一个方法running,但这两个方法不是同一个Function的实例,指向的内存也各自不同。
console.log(student.running === person.running); //false复制代码
- 都是做着同样的事情,但是每个实例都创建出一个方法,这就造成了没必要的内存损耗,那么有没有什么办法可以很好的解决这个问题呢,这时候,原型模式就出现了。
原型模式
- 每个函数都会创建一个prototype属性,这个属性是一个对象,该对象可以给每个实例共享属性和方法。
- 我们对之前的代码进行改造。
functionPerson(name, age) { this.name = name; this.age = age; } // 在这里,把方法running定义在了Person的prototype属性上// person和student共享同一个原型方法running,指向的是同一快内存空间Person.prototype.running = function (info) { console.log(info); }; const person = newPerson("moment", 18); const student = newPerson("supper", 16); // 在构造函数中这里输出的是falseconsole.log(person.running === student.running); //true person.running("我会跑步"); // 我会跑步 student.running("我会跑步,我比你还年轻呢"); //我会跑步,我比你还年轻呢复制代码
- 在前面的new操作符可以知道,person和student的隐式原型等于Perosn的显式原型
- 首先person和student现在自己身上查找有没有running方法,没有找到。
- 去原型里查找,也就是通过person.__proto__或者student.proto,该方法,由于person.proto=Person.prototype,所以调用person.running()实际上调用的是Person.prototype.running()方法
- 具体的内存表现形式如下图所示。
- 通过上图,一下代码的输出就能理解了。
console.log(Person.prototype.constructor === Person); // trueconsole.log(student.__proto__.constructor === Person); // true复制代码
原型层级
- 在讲之前先祭出两张神图
- 在讲原型之前,我们先来认识一下认识一下Function和Object的关系!
- 在JavaScript中,每个JavaScript函数实际上都是一个Function对象。
- 在 JavaScript 中,几乎所有的对象都是 Object 类型的实例,它们都会从 Object.prototype 继承属性和方法。
- 从原型链上讲,Function继承了Object。
- 从构造器上讲,Function构造了Object。
- 好了,看到这里,晕了没?
- 接下来通过代码的展示,应该能更清楚的说明了上面两张图中的所讲的意思了。
functionPerson(name, age) { this.name = name; this.age = age; } // 在这里,把方法running定义在了Person的prototype属性上了Person.prototype.running = function (info) { console.log(info); }; const obj = {}; const person = newPerson("moment", 18); const student = newPerson("supper", 16); console.log(obj.__proto__ === Object.prototype); // trueconsole.log(Object.constructor === Function); // trueconsole.log(Function.prototype.__proto__ === Object.prototype); // trueconsole.log(Function.constructor === Function); // trueconsole.log(Person.__proto__.constructor.__proto__ === Function.prototype); // trueconsole.log(student.__proto__.__proto__.constructor.__proto__ === Function.prototype); // trueconsole.log(Function.constructor.__proto__ === Function.prototype); // trueconsole.log(Object.constructor === Function.constructor); // trueconsole.log(ObjectinstanceofFunction); // trueconsole.log(FunctioninstanceofObject); // trueconsole.log(Function.prototype.__proto__ === Object.prototype); // true复制代码
- 原型查找机制,首先在实例上查找,有则返回,没有则往上查找,这时候查找到了原型上了,如果能找到有该属性或方法,则返回,如果仍然没找打,就在Object的原型上查找,找到了则返回,没有就返回undefined或者报错。
functionPerson(name, age) { this.name = name; this.age = age; this.sayName = function () { console.log("我是在实例身上的"); }; this.memory = "我是属于实例的"; } // 在这里,把方法running定义在了Person的prototype属性上了Person.prototype.running = function () { console.log("我是原型上的方法"); }; Object.prototype.牛逼 = "这是真的"; Person.prototype.memory = "我是属于原型的"; const person = newPerson("moment", 18); console.log(person.name); // 来自实例console.log(person.memory); // 来自实例 person.sayName(); // 来自实例 person.running(); // 来自原型console.log(person.牛逼); // 这是真的console.log(person.六六六); // undefined复制代码
原型模式的弊端
- 在继承之前,我们每个类都要给其定义属于它自己的属性和方法,但是如果出现相同的方法,我们都要给他重新定义一遍,这样未免出现过多重复代码。
functionStudent() {} functionTeacher() {} Student.prototype.running = function () { console.log("学生"); }; Student.prototype.eating = function () { console.log("吃"); }; Student.prototype.study = function () { console.log("学习"); }; Teacher.prototype.running = function () { console.log("老师 }; Teacher.prototype.teach = function () { console.log("吃"); }; 复制代码
原型链继承
- 父类的原型直接赋值给子类的原型
- 父类和子类共享同一个原型对象,修改了任意一个,另外一个也被修改。
- 这是一个错误的做法。
functionStudent() {} functionTeacher() {} Teacher.prototype.running = function () { console.log("老师"); }; Teacher.prototype.teach = function () { console.log("吃"); }; Student.prototype = Teacher.prototype; const student = newStudent(); student.running(); // 老师 student.__proto__.running = function () { console.log("我被修改了"); }; const teacher = newTeacher(); teacher.running();// 我被修改了复制代码
- 正确的原型链继承
functionStudent() {} functionTeacher() {} Teacher.prototype.running = function () { console.log("老师"); }; Teacher.prototype.teach = function () { console.log("教"); }; const teach = newTeacher(); Student.prototype = teach; const student = newStudent(); student.running = function () { console.log("我被修改了"); }; const smallStudent = newStudent(); smallStudent.running(); // 老师 继承于Teacher原型 student.running(); // 我被修改了 来自于实例本身复制代码
- 通过一张图来展示子类的两个实例和两个构造函数及其对应的原型之间的关系
- 原型链继承的问题
- 在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性和方法摇身一变成了原型属性和方法。
- 子类型在实例化时不能给父类型的构造函数传参。
盗用构造函数
- 通过call或者apply改变this指向
functionTeacher(nickname, age, height) { this.nickname = nickname; this.age = age; this.height = height; } functionStudent(nickname, age, height) { Teacher.call(this, nickname, age, height); this.hobby = ["唱", "跳", "rap"]; } Teacher.prototype.running = function () { console.log("老师"); }; Teacher.prototype.teach = function () { console.log("教"); }; const student = newStudent("moment", "18", "1米59"); console.log(student.height); // 1米59console.log(student.hobby); // ["唱", "跳", "rap"]复制代码
- 借用构造函数存在问题
- 必须在构造函数中定义方法,因此函数不能重用。
- 子类也不能访问父类原型上定义的方法。
组合继承
- 综合了原型链和盗用构造函数,将两者的优点集中了起来。
functionTeacher(nickname, age, height) { this.nickname = nickname; this.age = age; this.height = height; } functionStudent(nickname, age, height) { Teacher.call(this, nickname, age, height); this.hobby = ["唱", "跳", "rap"]; } Teacher.prototype.running = function () { console.log("老师"); }; Teacher.prototype.teach = function () { console.log("教"); }; Student.prototype = newTeacher(); const student = newStudent("moment", "18", "1米59"); console.log(student.height); // 1米59console.log(student.hobby); // ["唱", "跳", "rap"]复制代码
- 组合继承存在的问题 1.父类构造函数至少被调用两次。
引用类型继承终极解决方案 寄生式组合继承
functioninheritPrototype(superType, children) { const prototype = Object(superType.prototype); // 创建对象 prototype.constructor = children; // 增强对象 children.prototype = prototype; // 赋值对象 } functionTeacher(nickname, age, height) { this.nickname = nickname; } functionStudent(nickname) { Teacher.call(this, nickname); this.hobby = ["唱", "跳", "rap"]; } inheritPrototype(Student, Teacher); Teacher.prototype.running = function () { console.log("老师会跑步"); }; Student.prototype.running = function () { console.log("学生也会跑步"); }; const student = newStudent("moment"); student.running(); // 学生也会跑步console.log(student.hobby); // ['唱', '跳', 'rap']console.log(student.nickname); // comment复制代码
- 这里只调用了一次Teacher构造函数,避免了Student.prototype上不必要也用不到的属性。
- 原型链仍然保持不变。
大厂面试题分享 面试题库
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库