JS面向对象编程,原型与继承全面解析
面向对象编程的特点
- 封装:使用对象的人无需考虑内部实现,只考虑功能的使用。
- 继承:为了代码的可复用。
- 多态:不同对象 作用于同一操作产生不同结果。
JS如何创建对象
普通方式
const A = new Object()
A.attribute = '' // 定义属性
A.fn = function() { } // 定义方法
工厂模式
function Creat(attr) {
// .... 同上面普通方式
return A
}
const aa = Creat('...')
const bb = Creat('...')
存在问题:往实例化对象上层找不到父类,只能知道是一个Object
构造函数/实例
function Player(name) {
this.name = name
this.run = function() {
console.log('...');
}
}
const x = new Player('a')
const y = new Player('b')
console.log(x.run === y.run) // false 独立内存
缺点:通过this添加的属性和方法,总是指向当前对象(改变当前对象不会影响构造函数),实例化时通过this添加的属性和方法都会在内存当中复制一份。
原型对象
function Player(name) {
this.name = name
}
Player.prototype.run = function() {
console.log('...');
}
const x = new Player('a')
const y = new Player('b')
console.log(x.run === y.run) // true
静态属性
function Player(name) {
Player.count = 1
}
const p = new Player('a')
console.log(p.count) // undefined
console.log(Player.count) // 1
原型及原型链
查找原型对象的方法
xxx.__proto__
Object.getPrototypeOf(xxx)
new关键字做了什么
- 创建了新对象并将.__proto__指向构造函数的.prototype
- 将this指向新创建的对象
- 返回新对象(1. 有显式的返回值且是对象则返回这个对象 2. 其他情况返回this)
function newSimulator() {
// 1. 创建新对象
const obj = new Object()
// 2. 设置__proto__为构造函数prototype
const constructor = [].shift.call(arguments) // 取出参数第一项,并删除掉,剩余参数在下一步会用到
obj.__proto__ = constructor.prototype
// 3. this指向新对象,也就是改变this的指向:例如apply,call
const ret = constructor.apply(obj, arguments)
// 4. 返回新对象或this
return typeof ret === 'object' ? ret : obj
}
function Player(name, type) {
this.name = name
this.say = () => { console.log(`i am ${name},i create by ${type}`) }
}
const a = new Player('a', 'new')
a.say() // i am a,i create by new
const b = newSimulator(Player, 'b', 'newSimulator')
b.say() // i am b,i create by newSimulator
理解原型链
当读取实例对象的属性时,如果找不到,会查找对象原型中的属性,直到最上层为止。
Object.prototype.name = 'Object';
function Player() {}
Player.prototype.name = 'Player';
const p1 = new Player();
p1.name = 'p1';
console.log(p1.name); // p1
delete p1.name;
console.log(p1.name); // Player
delete Player.prototype.name;
console.log(p1.name); // Object
JS实现继承
原型链继承
function Parent() {
this.name = 'ParentName';
this.actions = ['sing', 'jump', 'rap'];
}
function Child() {}
Child.prototype = new Parent(); // 通过实例化对象来拿到全部属性方法
Child.prototype.constructor = Child; // 但是直接赋值会覆盖掉Child,这一步是其将修改回来
存在问题:引用类型被改变,所有实例共享,无法传参
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' ]
构造函数继承
function Parent(name) {
this.name = name;
this.actions = ['sing', 'jump', 'rap'];
this.say = function () {}
}
function Child() {
Parent.apply(this, arguments); // 把Parent执行了一遍,解决了传参问题
}
const c1 = new Child('c1');
const c2 = new Child('c2');
c1.actions.pop() // 引用类型的问题被解决
console.log(c1.name, c1.actions) // c1 [ 'sing', 'jump' ]
console.log(c2.name, c2.actions) // c2 [ 'sing', 'jump', 'rap' ]
console.log(c1.say === c2.say); // false 独立内存,构造函数的问题,消耗大
组合继承
该继承同时解决以上两种继承存在的问题,副作用是会重复执行构造函数
// 即原型链继承 + 构造函数继承
function Parent(name, actions) {
this.name = name;
this.actions = actions;
}
Parent.prototype.say = function () {
console.log(this.name + ' say');
}
function Child() {
Parent.apply(this, arguments); // 第一次调用构造函数
}
Child.prototype = new Parent(); // 第二次调用构造函数
Child.prototype.constructor = Child;
// 组合继承中到这一步,是使用到开头原型链继承的方式,可以看做它将Child中的原型链方法变成了引用类型
const c1 = new Child('c1', ['eat']);
const c2 = new Child('c2', ['run']);
c1.actions.pop() // 引用类型的问题被解决
console.log(c1.name, c1.actions) // c1 []
console.log(c2.name, c2.actions) // c2 [ 'run' ]
console.log(c1.say === c2.say); // true 内存消耗问题被解决
寄生组合式继承
// 还是刚才的组合继承,改变的地方会被注释
function Parent(name, actions) {
this.name = name;
this.actions = actions;
}
Parent.prototype.say = function () {
console.log(this.say + ' say');
}
function Child() {
Parent.apply(this, arguments);
}
// Child.prototype = new Parent();
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
解析其中 Object.create
等于以下写法(es5):
let TempFunction = function () {}; // 一个空的临时变量
TempFunction.prototype = Parent.prototype; // 将临时函数的原型指向Parent的原型
Child.prototype = new TempFunction(); // 这样就同样实现了原型链继承的优点,并且开销极低
这也就是“寄生”的含义:用一个纯粹的中间函数去执行了new。
es6继承
class Parent {
constructor() {
this.name = 'aaa';
}
say() {
console.log(this.name, 'say');
}
}
class Child extends Parent {
constructor() {
super();
}
}
JS:面向未来编程😋
以上就是文章的全部内容了,感谢看到这里!如果觉得写得还不错,对你有所帮助或启发,别忘了点赞收藏关注“一键三连”哦~ 我是茶无味的一天(m.palxp.cn)(公众号: 品味前端),一名平凡的前端 Developer,希望与你共同成长~