首先声明,我不是标题党,这是菜鸡本鸡的扪心自问。。今天被追问对面向对象的理解,并引申问了几个问题,没想到准备了那么久的八股文,居然也被问住了,看来是真的高估自己了,被挂得也心服口服。所以今天准备完整做个回顾,权当记录了。
请你谈谈你对面向对象的理解吧
面向对象其实是将问题抽象成具体的对象,这一个对象有自己的属性及方法,在解决问题的时候就是通过将对象结合在一起。建立对象的目的不是为了完整的完成某一个步骤,而是描述某个事物在整个解决问题的过程中的方法及行为。
请你说说面向对象的三个特性分别是什么
封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
继承:通过继承创建的新类称为“子类”或“派生类”。继承的过程,就是从一般到特殊的过程。
多态:同一个方法,面对不同的对象有不同的表现形式就叫做多态。( JS 中同一作用域下的同名函数,前者会被后者覆盖
请说说实现继承的方式
推荐一篇个人感觉写的不错的:面试官:Javascript如何实现继承? | web前端面试 - 面试官系列 (vue3js.cn)
可以先来几个问题测试下:
1)有几种继承的方式?优缺点是什么?
2)只有构造函数没有原型链的继承有什么优缺点?
3)为什么要有寄生式组合继承?
1. 法一:原型链继承
子类可以利用prototype将所有在父类中通过prototype追加的方法和属性都追加到子类。 下面是例子:
//声明父类
var SuperClass = function () {
this.name = "Mike";
};
//为父类添加共有方法
SuperClass.prototype.getSuperValue = function () {
return this.name();
};
//声明子类
var SubClass = function () {
this.sex = "female";
} };
//核心:继承父类,通过原型形成链条
SubClass.prototype = new SuperClass() ;
//为子类添加共有方法
SubClass.prototype.getSubValue= function () {
return this.sex()
};
缺点:
1)使用类继承的方法,如果父类的构造函数中有引用类型,就会在子类中被所有实例共用,因此一个子类的实例如果更改了这个引用类型,就会影响到其他子类的实例。
2)子类型实例不能给父类型构造函数传参
2. 法二:构造函数继承
通过使用call()或apply()方法,Parent构造函数在为Child的实例创建的新对象的上下文执行了,就相当于新的Child实例对象上运行了Parent()函数中的所有初始化代码,结果就是每个实例都有自己的info属性。
好处是即使改变了某一个对象的属性或方法,不会影响其他的对象(因为每一个对象都是复制的一份)。
function Parent() {
this.name = "Mike";
}
function Child() {
Parent.call(this)
}
缺点:
1)内存浪费。
2)call方法仅仅调用了父级构造函数的属性及方法,没有办法调用父级构造函数原型对象的方法。
3. 法三:组合继承
基本的思路就是使用原型链继承原型上的属性和方法,而通过构造函数继承实例属性,这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
//声明父类
var SuperClass = function (name) {
this.name = name;
this.sex=['male','female'];
};
//声明父类原型上的方法
SuperClass.prototype.sexs = function () {
console.log(this.books)
};
//声明子类
var SubClass = function (name) {
SuperClass.call(this, name)
};
//子类继承父类(链式继承)
SubClass.prototype = new SuperClass();
//实例化子类
var subclass1 = new SubClass('Mike');
var subclass2 = new SubClass('Tom');
法四:寄生式组合继承
父类的构造函数会被创建两次(call()的时候一遍,new的时候又一遍),所以为了解决这个问题,又出现了寄生组合继承。组合继承存在这一定的效率问题,它的父类构造函数始终会被调用俩次,一次在创建字类原型时调用,另一次在子类构造函数中调用。本质上子类只需要在执行时重写自己的原型就行了。
function inheritPrototype(subType, superType) {
let prototype = Object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
这个 inheritPrototype()函数实现了寄生式组合继承的核心逻辑。这个函数接收两个参数:子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的 prototype 对象设置 constructor 属性,解决由于重写原型导致默认 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。如下例所示,调用 inheritPrototype()就可以实现前面例子中的子类型原型赋值:
//声明父类
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
//声明父类原型上的方法
SuperType.prototype.sayName = function () {
console.log(this.name);
};
//声明子类
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {
console.log(this.age);
};
这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性, 因此可以说这个例子的效率更高。而且原型链仍然保持不变。
法五:寄生继承
寄生式继承就是用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。
// 寄生式继承
function subobject(obj) {
let clone = Object(obj);
clone.sayName = function(){
console.log("Mike")
};
return clone;
}
let sub = {
name:"Tom"
}
let sup = subobject(sub);
sup.sayName();//Mike
请说说封装的方式
通过构造函数添加
构造函数其实就是普通的函数,只不过有以下的特点
- 首字母大写
- 内部使用
this - 使用
new生成实例
通过this添加的属性和方法只在当前对象上添加,是该对象自身拥有的。 缺点:我们实例化一个新对象的时候,this指向的属性和方法都会得到相应的创建,即会在内存中复制一份,这样就造成了内存的浪费。
function Cat(name,color){
this.name = name;
this.color = color;
this.eat = function () {
alert('吃老鼠')
}
}
通过原型链
对于那些不变的属性和方法,我们可以直接将其添加在类的prototype 对象上,再生成实例即可。
在类的外部通过.语法添加
在实例化对象的时候,并不会执行到在类外部通过. 语法添加的属性和方法,所以实例化之后的对象是不能访问到. 语法所添加的对象和属性的,只能通过类自身访问。
用于js代码封装的设计模式有哪些?
参考:segmentfault.com/a/119000002…
主要有工厂模式,创建者模式,单例模式,原型模式四种。