以下题目是根据网上多份面经收集而来的,题目相同意味着被问的频率比较高,有问题欢迎留言讨论,喜欢可以点赞关注。
1、你是怎么理解面向对象的,什么是面向对象,用面向对象做过什么
面向对象编程(OOP,Object Oriented Programming)
用一个对象去描述一些属性和行为
image.png
好处:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
image.png2、继承(ES5/ES6),es5的继承实现一下,手写一个类的继承
https://www.jianshu.com/p/dd7eb3464759
es5 构造函数继承 原型链继承 组合继承 原子继承 原型方法继承 寄生式继承 寄生组合式继承
原型链继承继承
Cat.prototype = new Animal();
//上面的代码把cat的prototype指向了Animal 现在要还原回来
Cat.prototype.constructor = Cat;
// 问题
// 1、子类的构造函数的参数,没法传给父级的构造函数
// 2、子类的原型的constructor会被改变,需要自己变回来
// 3、父类使用this声明的属性被所有实例共享。 原因是实例化是父类一次性赋值到子类实例的原型上,它会将父类通过this声明的属性也赋值到子类原型上。例如在父类中一个数组值,在子类的多个实例中,无论哪一个实例去修改这个数组的值,都会影响到其他子类实例。
构造函数继承
function Cat(name, age) {
// Animal(age,name)//this===window;
// Animal.call(this) 无参数
Animal.call(this, name, age)//借用父类的构造函数 给子类创建实例属性
}
//优点:
//1、可以向父类传递参数。
//2、解决父类this声明的属性会被实例共享的问题。即实例修改属性不会影响到父级属性
//缺点
//1、不能继承父类prototype上的属性/方法,只能继承父类通过this声明的属性/方法。
//2、每次实例化子类,都要执行父类函数。重新声明父类所定义的方法,无法复用。
组合继承
规避了原型继承法(解决父类this声明的属性会被实例共享的问题)和构造函数继承法的缺点
// 父类
function Animal(name, age) {
this.name = name;
this.age = age;
this.fruit = ['water', 'apple']
}
// 在父类的原型上 创建run方法
Animal.prototype.run = function () {
console.log(this.name + ' running')
}
function Cat(name, age) {
// Animal(age,name)//this===window;
Animal.call(this, name, age)
}
Cat.prototype = new Animal();//组合原型继承模式
Cat.prototype.constructor = Cat;
var c = new Cat('Tom', 12)
console.dir(Cat.prototype.constructor)
console.log(c)// Tom
console.log(c.name)// Tom
console.log(c.age)// Tom
console.log(c.fruit)// ['water', 'apple']
优点
1、解决原型链继承父类this声明的属性或者方法被共享的问题。
2、解决借用构造函数解决不能继承父类prototype对象上的属性/方法问题。
缺点
1、调用了父类函数两次,造成一定的性能问题。
2、因调用两次父类,导出父类通过this声明的属性和方法被生成两份的问题。
3、原型链上下文丢失,子类和父类通过prototype声明的属性和方法都存在与子类prototype上
原子继承
var animal = { name: "qw", age: 8 };
var a = Object.create(animal);//'qw'
console.log(a.name);//{}
console.log(a);
// 注:此时a.name a.age可访问成功,但a本身并无此类属性,而a原型上有这些属性
// 优点:
// 不需要使用new构造函数就可以直接 构造另外其他对象
// 缺点:
// 所有构造函数出来的实例会共享 原型对象上的引用类型的属性
原型式继承
https://www.jianshu.com/p/ab6b8821e80f
function Object(o) {
function F(){};
// 将被继承的对象作为空函数的prototype
F.prototype = o;
// 返回new期间创建的新对象,此对象的原型为被继承的对象,
// 通过原型链查找可以拿到被继承对象的属性
return new F();
}
var o = { name: 'liu', age: 23 }
var m1 = Object(o)
console.log(m1.name)
优点
1、兼容性好,最简单的对象继承。
缺点
1、多少实例共享被继承的属性,存在被篡改的情况,不能传递参数。
寄生式继承
function Person(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
alert(this.name);
}
return o;
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
/* 重点:就是给原型式继承外面套了个壳子。
优点:没有创建自定义类型,因为只是套了个壳子返回对象,这个函数顺理成章就成了创建的新对象。
缺点:没用到原型,无法复用。 */
优点
1、兼容性好,最简单的对象继承。
缺点
1、多少实例共享被继承的属性,存在被篡改的情况,不能传递参数。
寄生组合继承
JS的继承方式有很多种,最理想的继承方式是寄生组合式继承。
组合继承(构造函数和原型的组合)会调用两次父类构造函数的代码,
function Person(name){
this.name=name;
}
Person.prototype.sayName=function(){
console.log(this.name+' '+this.gender+' '+this.age);
}
function Female(name,gender,age){
Person.call(this,name);//第一次调用父类构造函数
this.age=age;
this.gender=gender;
}
Female.prototype=new Person();//第一次调用父类构造函数
Female.prototype.constrcutor=Female;//因重写原型而失去constructor属性,所以要对constrcutor重新赋值
因此引入寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的方式来继承方法,而不需要为子类指定原型而调用父类的构造函数,我们需要拿到的仅仅是父类原型的一个副本。因此可以通过传入子类和父类的构造函数作为参数,首先创建父类原型的一个复本,并为其添加constrcutor,最后赋给子类的原型。这样避免了调用两次父类的构造函数,为其创建多余的属性。
function Parent(name) {
this.name = name
}
Parent.sayHello = function (){
console.log('hello')
}
Parent.prototype.sayName = function() {
console.log('my name is ' + this.name)
return this.name
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
function _inherits(Child, Parent) {
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
Child.__proto__ = Parent
}
_inherits(Child, Parent)
Child.prototype.sayAge = function () {
console.log('my age is ' + this.age)
return this.age
}
var parent = new Parent('Parent')
var child = new Child('Child', 18)
console.log(parent)
Parent.sayHello()
parent.sayName()
console.log(child)
Child.sayHello()
child.sayAge()
child.sayName()
1、寄生组合式继承是当前最成熟的继承方法,也是先也常用的继承方法,在大多数Js框架中都是用这个作为继承方案。
寄生组合式继承相对组合继承的优点:
1、只调用了父类构造函数一次,节约了性能。
2、避免生成了不必要的属性。
3、使用原型式继承保证了原型链上下文不变,子类的prototype只有子类通过prototype声明的属性和方法,父类的prototype只有父类通过prototype声明的属性和方法。
es6通过extend
1、class 可以理解为function,由于class本质还是一个function,因此它也会拥有一个的prototype属性,当new一个class时,会把class的porototype属性赋值给这个新对象的 __proto属性。
2、constructor 方法是默认添加的方法,在new一个对象时,自动调用该方法,constructor里面定义自己的属性。
3、继承extends和super,class 子类名 extends 父类名实现继承,当然还得在constructor里面写上super(父类的参数),意思就是在子类中获得父类的this指针,相当于Animal.call(this)
// es6继承
class Animal {
//构造函数,里面写上对象的属性
constructor(props) {
this.name = props.name || 'Unknown';
}
//方法写在后面
eat() {//父类共有的方法
console.log(this.name + " will eat pests.");
}
}
//class继承
class Bird extends Animal {
//构造函数
constructor(props,myAttribute) {//props是继承过来的属性,myAttribute是自己的属性
//调用实现父类的构造函数
super(props)//相当于获得父类的this指向
this.type = props.type || "Unknown";//父类的属性,也可写在父类中
this.attr = myAttribute;//自己的私有属性
}
fly() {//自己私有的方法
console.log(this.name + " are friendly to people.");
}
myattr() {//自己私有的方法
console.log(this.type+'---'+this.attr);
}
}
//通过new实例化
var myBird = new Bird({
name: '小燕子',
type: 'Egg animal'//卵生动物
},'Bird class')
myBird.eat()
myBird.fly()
myBird.myattr()
3、JS的继承方法x2,用原型实现继承有什么缺点,怎么解决
同2题,继承最大的优点就是能继承父类构造函数和原型的属性和方法
缺点:
1、子类的构造函数的参数,没法传给父级的构造函数
2、子类的原型的constructor会被改变,需要自己变回来
3、父类使用this声明的属性被所有实例共享。 原因是实例化是父类一次性赋值到子类实例的原型上,它会将父类通过this声明的属性也赋值到子类原型上。例如在父类中一个数组值,在子类的多个实例中,无论哪一个实例去修改这个数组的值,都会影响到其他子类实例。
方法:实用组合继承
4、解释一下原型和原型链有什么特点x2
原型
每个函数都有一个prototype属性,这个属性指向的就是原型对象. 实例有一个proto指向它构造函数的原型对象.
原型链
当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,会去对象的proto属性(隐式原型对象)里去找 , (这里的隐式原型对象指向的就是它构造函数的prototype(显示原型对象))然后原型本身也是一个对象 , 拥有proto 属性 , 所以会继续向上查找 ,一直找到Object.prototype.proto===null 这样的链条称之为原型链
特点
原型对象上的方法是被不同实例共有的 ,当我们修改原型时,与之相关的对象也会继承这一改变。原型链就是为了实现继承
5、介绍this和原型
一:到底什么是this呢
概念:执行上下文,this一般存在于函数中,表示当前函数的执行上下文
, 如果函数没有执行,那么this没有内容,只有函数在执行后this才有绑定。注意:this的指向只能是对象,当然别忘记了数组也是一个特殊的对象。
二:this到底指向的是谁呢
this的指向其实是跟this的执行位置是有关的,不同的位置执行位置,this的指向就可能发生改变。this被谁执行了,this就是执行谁的,这时候一定要看清楚this是被谁直接执行的! 默认执行:this指向了window,在严格模式下,this指向了undefined
image.png6、使用原型最大的好处
原型的作用一:数据共享,节省空间,继承
利用原型可以使得每一个实例对象都可以调用原型上的say方法,可以让每个实例对象只是得到函数say的一个指针,指向同一个say函数,节省了空间
原型的作用二:继承
在子类构造函数中借用父类构造函数,再通过原型继承父类的原型属性和方法,模拟继承的效果
7、prototype和_proto_区别x2
image.pngimage.png image.png
对象并不具有prototype属性,只有函数才有prototype属性。
总结:
js里所有的对象都有proto属性(对象,函数),指向构造该对象的构造函数的原型。
只有函数function才具有prototype属性。这个属性是一个指针,指向一个对象,这个对象的用途就是包含所有实例共享的属性和方法(我们把这个对象叫做原型对象)。原型对象也有一个属性,叫做constructor,这个属性包含了一个指针,指回原构造函数。
8、construct是什么
construct构造属性构造属性存储着这个对象所对应的函数的引用。由于JS对象是通过call调用并执行堆的声明函数实现的JS对象,所以JS对象的construct存储着这个JS对象所调用的函数的函数引用
<script type="text/javascript">
function employee(name,job,born)
{
this.name=name;
this.job=job;
this.born=born;
}
var bill=new employee("Bill Gates","Engineer",1985);
document.write(bill.constructor);
console.log(bill.__proto__.constructor);//结果同上
</script>
输出:
function employee(name, job, born)
{this.name = name; this.job = job; this.born = born;}
9、new 的过程,new的原理是什么?通过new的方式创建对象和通过字面量创建有什么区别,实现new
image.png image.png10、ES5对象 vs ES6对象(es6 class 的new实例和es5的new实例有什么区别?)
在ES6
中(和ES5
相比),class
的new
实例有以下特点:
-
class
的构造参数必须是new
来调用,不可以将其作为普通函数执行 -
es6
的class
不存在变量提升 -
最重要的是:
es6
内部方法不可以枚举。es5的prototype
上的方法可以枚举。
为此我做了以下测试代码进行验证:
console.log(ES5Class()) // es5:可以直接作为函数运行
// console.log(new ES6Class()) // 会报错:不存在变量提升
function ES5Class(){
console.log("hello")
}
ES5Class.prototype.func = function(){ console.log("Hello world") }
class ES6Class{
constructor(){}
func(){
console.log("Hello world")
}
}
let es5 = new ES5Class()
let es6 = new ES6Class()
console.log("ES5 :")
for(let _ in es5){
console.log(_)
}
// es6:不可枚举
console.log("ES6 :")
for(let _ in es6){
console.log(_)
}
复制代码
这篇《JavaScript创建对象—从es5到es6》对这个问题的深入解释很好,推荐观看!
11、介绍class和ES5的类以及区别
ES5的继承实质是先创建子类的实例对象this,然后将父类的方法添加到this上。
ES6的继承实质是先将父类实例对象的方法和属性加到this上面,然后在用子类的构造函数修改this。
ES5的继承是通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。ES6的继承机制实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。具体为ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其调用。如果不调用super方法,子类得不到this对象。注意:super关键字指代父类的实例,即父类的this对象。在子类构造函数中,调用super后,才可使用this关键字,否则报错。
12、call,apply,bind 三者用法和区别:角度可为参数、绑定规则(显示绑定和强绑定),运行效率、运行情况,原生实现bind
https://juejin.im/post/59bfe84351882531b730bac2
fun.apply(thisArg, [argsArray])
thisArg:在 fun 函数运行时指定的 this 值。需要注意的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。
argsArray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 fun 函数。如果该参数的值为null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。浏览器兼容性请参阅本文底部内容。
首先说明call,apply是ES5中的语法,bind是ES6新引入的,它们三者的相似之处为:
- 都是用来改变函数的this对象的指向(即函数运行时的上下文(context))
- 第一个参数都是this要指向的对象
- 都可以利用后续参数进行传参
apply方法实际上是与call方法用法相同,只不过apply方法传进去的参数是以数组形式(或者类数组)bind()的作用其实与call()以及apply()都是一样的,都是为了改变函数运行时的上下文,bind()与后面两者的区别是,call()和apply()在调用函数之后会立即执行,而bind()方法调用并改变函数运行时的上下文的之后,返回一个新的函数,在我们需要调用的地方去调用他。
call,apply,bind 三者用法和区别:参数、绑定规则(显示绑定和强绑定)、运行效率(最终都会转换成一个一个的参数去运行)、运行情况(call,apply 立即执行,bind 是return 出一个 this “固定”的函数,这也是为什么 bind 是强绑定的一个原因)。
注:“固定”这个词的含义,它指的固定是指只要传进去了 context,则 bind 中 return 出来的函数 this 便一直指向 context,除非 context 是个变量。
实现bind
Function.prototype.myBind = function (obj) {
const object = obj || window; //如果第一个参数为空则默认指向window对象
let self = this;
let args = [...arguments].slice(1); //存放参数的数组
return function () {
let newArgs = [...arguments]
return self.apply(object, args.concat(newArgs))
}
}
personOne.say.myBind(personTwo, "女", 24)();
前面的知识不重复说,return function是因为bind返回的是一个函数,并且这个函数不会执行,需要我们再次调用,那么当我们调用的时候,我们依旧可以对这个函数进行传递参数,即为支持柯里化形式传参,所以需要在返回的函数中声明一个空的数组接收调用bind函数返回的函数时传递的参数,之后对两次的参数使用concat()方法进行连接,调用ES5中的apply方法。
介绍下原型链(解决的是继承问题吗)
请简述原型链
https://juejin.im/post/5d282541e51d4577523f2422
image.png
function employee(name, job, born) {
this.name = name;
this.job = job;
this.born = born;
}
var b = new employee("Bill Gates", "Engineer", 1985);
employee.prototype.age = 200
console.log('age' in b);//true
console.log(b.hasOwnProperty('name'));//true
console.log(b.hasOwnProperty('age'));//false 原型中的属性不能检测到
console.log(b.age !== undefined);//true
网友评论