JavaScript 高级程序设计
原型链与面向对象编程
this 指向、闭包和作用域
Promise规范及应用
手写JS 常见 API
本文档使用 MrDoc 发布
-
+
首页
原型链与面向对象编程
## 什么是面向对象编程 面向对象是一种编程思想。简单说,面向过程关注的是动词,分析出解决问题需要的步骤,然后写函数实现每个步骤,依次调用。 而 `面向对象关注的是主谓,是把构成问题的事物拆解为各个对象,而拆解出对象的目的不是为了实现某个步骤,而是为了描述这个事物在当前问题中的各种行为`。 它的三大特点: 1. 封装:让使用对象的人不用考虑内部实现,只考虑功能使用。把内部代码保护起来,只留出一些 api 接口提供用户使用 2. 继承:是为了代码的复用,从父类上继承一些方法和属性,子类也有一些自己的属性和方法 3. 多态:是不同对象作用于同一操作,产生不同的效果。把 `想做什么和谁去做分开` 多态小案例 ```js var makeSound = function(animal) { // 我不管你是什么动物,会叫就执行。此处表达了,不同对象在同一个操作,产生了不同的效果 animal.sound(); } var Duck = function(){} // 鸭叫是鸭子的行为 Duck.prototype.sound = function() { console.log('嘎嘎嘎') } var Chiken = function() {}; // 鸡叫是鸡的行为 Chiken.prototype.sound = function() { console.log('咯咯咯') } makeSound(new Chicken()); makeSound(new Duck()); ``` ## js 中的面向对象 js 中的面向对象包含方法和属性 ### 创建对象 创建一个对象有哪些方式? 1. 直接创建 ```js const Player = new Object(); Player.color = "white"; Player.start = function () { console.log("white下棋"); }; ``` 特点:无法识别对象类型,每新建一个对象就要再次开辟一个内存空间 2. 通过构造函数/实例创建 ```js function Player(color) { this.color = color; this.start = function () { console.log(color + "下棋"); }; } const whitePlayer = new Player("white"); const blackPlayer = new Player("black"); ``` 特点:通过 this 添加属性和方法,实例化的时候会在内存中被复制一遍,造成内存浪费。但是这样创建的好处是改变某一个对象的属性或方法不会影响其他对象。 3. 使用原型创建 ```js function Player(color) { this.color = color; } Player.prototype.start = function () { console.log(color + "下棋"); }; const whitePlayer = new Player("white"); const blackPlayer = new Player("black"); ``` 特点: 通过原型继承的方法,并不是自身的,需要在原型链上一层层的找,这样创建的好处是内存中只需要创建一次,实例化的对象,都会指向这个 prototype 对象。 4. 使用静态属性创建 ```js function Player(color) { this.color = color; if (!Player.total) { Player.total = 0; } Player.total++; } let p1 = new Player("white"); console.log(Player.total); // 1 let p2 = new Player("black"); console.log(Player.total); // 2 ``` 特点:是绑定在构造函数上的属性方法,需要通过构造函数访问。 ## 原型及原型链 ### 原型 函数的原型对象就是 prototype,原型对象里有一个 constructor,就是函数本身,使用 new 关键字实例化时,会生成新的对象,新的实例对象的__proto__ === 函数的 prototype ```js function Player(color) { this.color = color; } Player.prototype.start = function () { console.log(color + "下棋"); }; const whitePlayer = new Player("white"); const blackPlayer = new Player("black"); console.log(blackPlayer.__proto__); // Player {} console.log(Object.getPrototypeOf(blackPlayer)); // Player {},可以通过 Object.getPrototypeOf来获取__proto__ console.log(Player.prototype); // Player {} console.log(Player.__proto__); // [Function] ``` new 关键字做了什么? 1. 一个继承自 Player.prototype 的新对象 whitePlayer 被创建 2. whitePlayer.proto 指向 Player.prototype 3. 将 this 指向新创建的对象 whitePlayer 4. 返回一个新的对象 * 如果构造函数没有显示的返回值,则返回 this * 如果构造函数有显示返回值,是基本类型,还是返回 this * 如果构造函数返回有显示返回值,是对象类型,比如{a:1},则返回这个对象 手写实现 new 关键字 ```js function mockNew(Constructor,...args){ // 使用函数原型创建一个新的对象obj const obj = Object.create(Constructor.prototype); // 将函数的this指向修改到obj,并且执行 let ret = Constructor.apply(obj, args) // 返回值如果是对象直接返回执行结果,不是就返回obj return typeof res === 'object' ? res : obj; } ``` ### 原型链 当读取实例属性时,找不到,就会查找与对象关联的原型中的属性,如果还查不到,留会找原型的原型,一直找到最顶层为止,这个过程叫原型链。 代码演示 ```js Object.prototype.name = "root"; function Player() {} Player.prototype.name = "Kevin"; var p1 = new Player(); p1.name = "Daisy"; // 查找p1对象中的name属性,因为上⾯添加了name,所以会输出“Daisy” console.log(p1.name); // Daisy delete p1.name; // 删除了p1.name,然后查找p1发现没有name属性,就会从p1的原型p1.__proto__中去找,也就是Player.prototype,然后找到了name,输出"Kevin" console.log(p1.name); // Kevin delete Player.prototype.name; console.log(p1.name); // root ``` ## 继承 继承的意义是,儿子继承父亲的属性或函数 ### 原型链继承 ```js function Parent() { this.name = 'ParentName'; this.actions = ['sing', 'jump', 'rap']; } function Child() {} // 此时Child可以调用getName,但还不能调用name和actions Child.prototype = new Parent(); // 由于constructor在prototype之下,上一个步骤改变了Child.prototype.constructor为Parent,所以需要重新把Child赋值给constructor Child.prototype.constructor = Child; // Child实例化之后, 可以获取到name、action, 因为从原型链上找 // c1.__proto__ === Child.prototype,如果没找到?,继续找 // Child.prototype.__proto__ === Parent.prototype // 到此处Child实现了继承Parent的方法和属性 const c1 = new Child(); c1.actions.push('basketball'); console.log(c1.actions); //[ 'sing', 'jump', 'rap', 'basketball' ] const c2 = new Child(); console.log(c2.actions); //[ 'sing', 'jump', 'rap', 'basketball' ] ``` 隐藏了两个问题, 需要解决 1. 如果有属性是引用类型,一旦被修改了,多个实例都会被改变 2. 创建 child 实例的时候无法传参,提供给 Parent 使用 ### 构造函数继承 针对问题: 1. 如果有属性是引用类型,一旦被修改了,多个实例都会被改变 ```javascript function Parent() { this.actions = ["eat", "run"]; this.name = "parentName"; } function Child() { // 我们通过call函数来调用Parent,this指针改变,达到数据独立的效果 Parent.call(this); } const child1 = new Child(); const child2 = new Child(); child1.actions.pop(); console.log(child1.actions); // ['eat'] console.log(child1.actions); // ['eat', 'run'] ``` 2. 如何传参? ```javascript function Parent(name, actions) { this.actions = actions; this.name = name; this.eat = function () { console.log(`${name} - eat`); }; } function Child(id, name, actions) { Parent.call(this, name); // 如果想直接传多个参数, 可以Parent.apply(this, Array.from(arguments).slice(1)); this.id = id; } const child1 = new Child(1, "c1", ["eat"]); const child2 = new Child(2, "c2", ["sing", "jump", "rap"]); console.log(child1.name); // { actions: [ 'eat' ], name: 'c1', id: 1 } console.log(child2.name); // { actions: [ 'sing', 'jump', 'rap' ], name: 'c2', id: 2 } console.log(child1.eat === child2.eat); // false ``` 隐藏一个问题 1. 当构造函数内有方法被定义时,每次实例化都会创建一个新的函数,多占用一块内存 ### 组合继承 针对这个问题 1. 当构造函数内有方法被定义时,每次实例化都会创建一个新的函数,多占用一块内存 ```javascript function Parent(name, actions) { this.name = name; this.actions = actions; } // 我们使用原型链来添加方法 Parent.prototype.eat = function () { console.log(`${this.name} - eat`); }; function Child(id) { // 使用apply函数来做传参使用 Parent.apply(this, Array.from(arguments).slice(1)); this.id = id; } Child.prototype = new Parent(); Child.prototype.constructor = Child; const child1 = new Child(1, "c1", ["hahahahahhah"]); const child2 = new Child(2, "c2", ["xixixixixixx"]); child1.eat(); // c1 - eat child2.eat(); // c2 - eat console.log(child1.eat === child2.eat); // true ``` 隐藏一个问题 1. 调用了两次构造函数 * Parent.apply(this, Array.from(arguments).slice(1)); * Child.prototype = new Parent(); ### 寄生组合式继承 针对问题 1. 调用了两次构造函数,解决思路是,用 prototype 代替 new Parent ```javascript // 原型链继承 + 构造函数继承 // 1. 引用类型被改变,所有实例共享 // 2. 无法传参 // 3. 多占用了内存空间 function Parent(name, actions) { this.name = name; this.actions = actions; } Parent.prototype.getName = function () { console.log(this.name + '调用了getName'); } function Child() { Parent.apply(this, arguments); } // Child.prototype = Parent.prototype; // 一旦更改Child.prototype,Parent.prototype也会被修改。 Child.prototype = Object.create(Parent.prototype); // 使用原型链继承 // Child.prototype = new Parent(); // let TempFunction = function () {}; // TempFunction.prototype = Parent.prototype; // Child.prototype = new TempFunction(); Child.prototype.constructor = Child; // 改变构造函数为Child // super() Child const c1 = new Child('c1', ['eat']); const c2 = new Child('c2', ['run']); ```
追风者
2022年3月3日 22:56
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
关于 MrDoc
觅思文档MrDoc
是
州的先生
开发并开源的在线文档系统,其适合作为个人和小型团队的云笔记、文档和知识库管理工具。
如果觅思文档给你或你的团队带来了帮助,欢迎对作者进行一些打赏捐助,这将有力支持作者持续投入精力更新和维护觅思文档,感谢你的捐助!
>>>捐助鸣谢列表
微信
支付宝
QQ
PayPal
Markdown文件
分享
链接
类型
密码
更新密码