侧边栏壁纸
博主头像
落叶人生博主等级

走进秋风,寻找秋天的落叶

  • 累计撰写 124527 篇文章
  • 累计创建 28 个标签
  • 累计收到 9 条评论
标签搜索

目 录CONTENT

文章目录

面向对象编程及继承

2022-05-16 星期一 / 0 评论 / 0 点赞 / 333 阅读 / 13608 字

⾯向对象是⼀种编程思想,经常被拿来和⾯向过程比较。
面向过程关注的重点是动作,是分析出解决问题需要的步骤,然后编写函数实现每个步骤,最后依次调用函数。
⾯向对象关注的重点是对象,是把构成问题的事物拆解为各个对象,⽽拆解出对象的目的也不是为了实现某个步骤,⽽是为了描述这个事物在当前问题中的各种行为。

特点: 封装:让使⽤对象的人不考虑内部实现,只考虑功能使用,把内部的代码保护起来,只留出⼀些 api 接口供⽤户使用 继承:就是为了代码的复⽤,从⽗类上继承出一些⽅法和属性,⼦类也有⾃己的⼀些属性 多态:是不同对象作⽤于同⼀操作产⽣不同的效果。多态的思想实际上是把“想做什么”和“谁去做“分开 什么时候使⽤⾯向对象? 对于比较复杂的问题或者参与⽅较多的时候,⾯向对象的编程思想可以很好的简化问题,并且能够更好的扩展和维护。 对于比较简单的问题,⾯向对象和面向过程其实差异并不明显。 对象包含什么? 方法、属性 ⼀些内置对象 Object Array Date Function RegExp 创建对象 1. new Object() 每个新对象都要重新写⼀遍 color 和 start 的赋值 const Player = new Object(); Player.color = "white"; Player.start = function () { console.log("white下棋"); }; 2. 对象字面量 new Object()和对象字面量的方法在使用同一接口创建多个对象时,会产生大量重复代码,为了解决此问题,工厂模式被开发。 const Player = { color:'white', start: function () { console.log("white下棋"); }; } 3. 工厂模式 工厂模式解决了重复实例化多个对象的问题,但没有解决对象识别的问题(无法识别对象的类型,因为全部都是Object,不像Date、Array等,本例中,得到的都是Player对象,对象的类型都是Object,因此出现了构造函数模式)。 function createObject() { const Player = new Object(); Player.color = "white"; Player.start = function () { console.log("white下棋"); }; return Player; } 4. 构造函数/实例 通过 this 添加的属性和⽅法总是指向当前对象的,所以在实例化的时候,通过 this 添加的属性和⽅法都会在内存中复制⼀份,这样就会造成内存的浪费。 但是这样创建的好处是即使改变了某⼀个对象的属性或⽅法,不会影响其他的对象 function Player(color) { this.color = color; this.start = function () { console.log(color + "下棋"); }; } const whitePlayer = new Player("white"); const blackPlayer = new Player("black"); Tips. 怎么看函数是不是在内存中创建了多次呢? 我们可以直接比较 whitePlayer.start === blackPlayer.start // 输出 false 5. 原型 通过原型继承的⽅法并不是⾃身的,我们要在原型链上⼀层⼀层的查找,这样创建的好处是只在内存中创建⼀次,实例化的对象都会指向这个 prototype 对象。 function Player(color) { this.color = color; } Player.prototype.start = function () { console.log(color + "下棋"); }; const whitePlayer = new Player("white"); const blackPlayer = new Player("black"); 6. 混合模式(构造函数模式+原型模式) 构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性 function Person(name,age,family){ this.name = name; this.age = age; this.family = family; } Person.prototype = { constructor: Person, // 每个函数都有prototype属性,指向该函数原型对象,原型对象都有constructor属性,这是一个指向prototype属性所在函数的指针 say: function(){ alert(this.name); } } var person1 = new Person("lisi",21,["lida","lier","wangwu"]); console.log(person1); var person2 = new Person("wangwu",21,["lida","lier","lisi"]); console.log(person2); 可以看出,混合模式共享着对相同方法的引用,又保证了每个实例有自己的私有属性。最大限度的节省了内存。 静态属性 是绑定在构造函数上的属性方法,需要通过构造函数访问,比如我们想看⼀下一共创建了多少个玩家的实例 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 原型及原型链 在原型上添加属性/方法有什么好处? 如果不通过原型的方式,每生成⼀个新对象,都会在内存中新开辟⼀块存储空间,当对象变多之后,性能会变得很差。 Player.prototype.xx = function () {}; 这种方式向原型对象添加属性或者方法的话,⼜显得⾮常麻烦。所以我们可以这样写,可以通过Object.getPrototypeOf()获取对象的原型 Player.prototype = { start: function () { console.log("下棋"); }, revert: function () { console.log("悔悔棋"); }, }; new关键字做了什么? 创建一个新对象 将新对象的__proto__指向构造函数的prototype >> 为了访问构造函数原型上的属性&方法 将构造函数的this指向新对象的this >> 为了访问构造函数的自身属性&方法 返回新对象 • 如果构造函数没有显式返回值或返回基本类型,⽐如 number,string,boolean, 返回 this • 如果构造函数有显式返回值,是对象类型,比如{ a: 1 }, 则返回这个对象{ a: 1 } // 1. ⽤new Object() 的⽅式新建了⼀个对象 obj // 2. 取出第⼀个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数 // 3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性 // 4. 使⽤ apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性 // 5. 返回 obj function objectFactory() { let obj = new Object(); let Constructor = [].shift.call(arguments); obj.__proto__ = Constructor.prototype; let ret = Constructor.apply(obj, arguments); return typeof ret === "object" ? ret : obj; } 原型链是什么? 当读取实例属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,⼀直找到最顶层Object为止,如果还没有就返回undefined。 这样⼀条通过 proto 和 prototype 去连接的链条,就是原型链。 继承 1、原型链继承 function Parent() { this.name = "parentName"; } Parent.prototype.getName = function () { console.log(this.name); }; function Child() {} // Parent的实例同时包含实例属性⽅法和原型属性⽅法,所以把new Parent()赋值给 Child.prototype。 // 如果仅Child.prototype = Parent.prototype,那么Child只能调用getName,无法调用.name // 当Child.prototype = new Parent()后, 如果new Child()得到⼀个实例对象child,那么 // child.__proto__ === Child.prototype; // Child.prototype.__proto__ === Parent.prototype // 也就意味着在访问child对象的属性时,如果在child上找不到,就会去Child.prototype去找,如果还找不到,就会去Parent.prototype中去找,从而实现了继承。 Child.prototype = new Parent(); // 因为constructor属性是包含在prototype⾥的,上⾯重新赋值了了prototype,所以会导致Child的constructor指向[Function: Parent], // 有的时候使用child1.constructor判断类型的时候就会出问题 // 为了保证类型正确,我们需要将Child.prototype.constructor 指向他原本的构造函数Child Child.prototype.constructor = Child; var child1 = new Child(); child1.getName(); // parentName 问题: 如果有属性是引用类型,⼀旦某个实例修改了这个属性,所有实例都会受影响 创建 Child 实例时,不能传参 function Parent() { this.actions = ["eat", "run"]; } function Child() {} Child.prototype = new Parent(); Child.prototype.constructor = Child; const child1 = new Child(); const child2 = new Child(); child1.actions.pop(); console.log(child1.actions); // ['eat'] console.log(child2.actions); // ['eat'] 2、构造函数继承 针对问题 1. 我们可以使⽤用 call 来复制一遍 Parent 上的操作 function Parent() { this.actions = ["eat", "run"]; this.name = "parentName"; } function Child() { 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. 我们应该怎么传参呢? function Parent(name, actions) { this.actions = actions; this.name = name; } 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 } 问题 属性或方法想被继承的话,只能在构造函数中定义。而如果⽅法在构造函数内定义了,那每次创建实例都会创建⼀遍方法,多占一块内存。 function Parent(name, actions) { this.actions = actions; this.name = name; this.eat = function () { console.log(`${name} - eat`); }; } function Child(id) { Parent.apply(this, Array.prototype.slice.call(arguments, 1)); this.id = id; } const child1 = new Child(1, "c1", ["eat"]); const child2 = new Child(2, "c2", ["sing", "jump", "rap"]); console.log(child1.eat === child2.eat); // false 3、组合继承 通过原型链继承我们实现了基本的继承,⽅法存在 prototype 上,⼦类可以直接调用。但引⽤类型的属性会被所有实例共享,且不能传参。 通过构造函数继承,我们解决了上⾯2个问题:使用 call 在子构造函数内重复⼀遍属性和方法的创建,且支持传参了,但构造函数内部存在重复创建,占用内存过多。 所以我们将这两种⽅式结合起来,这就叫做组合继承。 function Parent(name, actions) { this.name = name; this.actions = actions; } Parent.prototype.eat = function () { console.log(`${this.name} - eat`); }; function Child(id) { 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 问题 调⽤了2次构造函数,做了重复的操作 Parent.apply(this,Array.from(arguments).slice(1)); Child.prototype = new Parent(); 4、寄生组合式继承 针对调用2次构造函数的问题,我们可以考虑让 Child.prototype 间接访问到 Parent.prototype function Parent(name, actions) { this.name = name; this.actions = actions; } Parent.prototype.eat = function () { console.log(`${this.name} - eat`); }; function Child(id) { Parent.apply(this, Array.from(arguments).slice(1)); this.id = id; } // 模拟Object.create的效果 let TempFunction = function () {}; TempFunction.prototype = Parent.prototype; Child.prototype = new TempFunction(); // 以上几行直接用Object.create的话,可写成Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; const child1 = new Child(1, "c1", ["hahahahahhah"]); const child2 = new Child(2, "c2", ["xixixixixixx"]); 直接 Child.prototype = Parent.prototype 不⾏吗? 答:不⾏不行不行!!这样做的话,修改子构造函数的prototype会影响父构造函数的prototype function Parent(name, actions) { this.name = name; this.actions = actions; } Parent.prototype.eat = function () { console.log(`${this.name} - eat`); }; function Child(id) { Parent.apply(this, Array.from(arguments).slice(1)); this.id = id; } Child.prototype = Parent.prototype; Child.prototype.constructor = Child; console.log(Parent.prototype, '之前'); // Child { eat: [Function], childEat: [Function] } Child.prototype.childEat = function () { console.log(`childEat - ${this.name}`); }; const child1 = new Child(1, "c1", ["hahahahahhah"]); console.log(Parent.prototype, '之后'); // Child { eat: [Function], childEat: [Function] } 可以看到,在给 Child.prototype 添加新的属性或者方法后,Parent.prototype 也会随之改变,这可不是我们想看到的。 5、class继承 ES6之后我们可以直接用class实现继承,要注意的是,子类必须在constructor方法中调用super方法,否则会报错。这是因为子类自己的this对象必须先通过父类的构造函数完成塑造,然后再加上子类自己的实例属性和方法。 ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面Parent.apply(this),它与ES6的继承机制完全不同。 在子类的构造函数中,只有调用super之后,才能使用this关键字,否则会报错 class Parent { constructor(x, y) { this.x = x; this.y = y; } } class Child extends Parent { constructor(x, y, color) { this.color = color; // ReferenceError super(x, y); // 必须先执行一下super this.color = color; // 正确 } }

. .

广告 广告

评论区