likes
comments
collection
share

前端面试基础-面向对象

作者站长头像
站长
· 阅读数 17

前言

记录前端面向对象的学习 学习已完成

  • 1.面向对象
  • 2.JS创建对象的几种方式
  • 3.JS实现继承的几种方式
  • 4.JS实现深克隆
  • 5.this指向的几种情况

1.面向对象

面向对象三大特点(封装、继承、多态)

1.封装

定义:创建一个对象,集中保存现实中一个事物的属性和功能。零散的数据封装进对象结构中,便于大量数据的管理维护。

a.封装对象的第一种方式 - 直接量

var obj={ // new Object()的简写
    a: 1,
    b: 2
    fn: function(){ 
        console.log(this.a)
    }
}

b.封装对象的第二种方式 - new Object()

var obj= new Object();
obj.a = 1;
obj.fn = function(){ 
    console.log(this.a)
}

obj.a 的访问方式 揭示了JS语言底层最核心的原理:JS中所有对象底层都是关联数组 前端面试基础-面向对象

    1. 存储结构: 都是键值对的组合 \
    1. 访问成员时:
    • 标准写法都是: 对象名/数组名["成员名"]
    • 简写都是: 对象名/数组名.成员名吗,比如: obj[“a”]和obj.a都行
    1. 强行给不存在的位置赋值不但不会报错,而且还会自动添加该属性。所以给对象添加一个新属性或新方法,都可通过强行赋值的方式。\
    1. 强行访问不存在的位置的值都不报错而是返回undefined
    1. 都可以用 for in 循环遍历

c.封装对象的第三种方式 - 构造函数

function Obj(a) {
    this.a = a;
    this.b = b;
    this.fn = function(){
        console.log(this.b)
    }
}
var obja = new Obj(1) // 使用构造函数可以反复创建多个对象

new做了4件事

  • 1. 创建一个新的空对象
  • 2. 让子对象继承构造函数的原型对象(继承看下面)
  • 3. 调用构造函数
    • 将构造函数中的this -> new 创建的新对象
    • 在构造函数内通过强行赋值方式,为新对象添加规定的属性和方法
  • 4. 返回新对象的地址,保存到=左边的变量里

总结this指向两种情况:

  • obj.fun() fun中的this->.前的obj对象
  • new Fun() Fun中的this->new创建的新对象

2.继承

抛出问题:方法定义放在构造函数中,每次new时都会执行function,就会反复创建相同函数的多个副本!浪费内存 解决:所以多个子对象都要使用相同的功能和属性值时,都可以用继承来解决 定义:父对象中的成员,子对象无需重复创建,就可直接使用!就像使用自己的成员一样!this.属性名/方法名()

JS中的继承都是通过原型对象实现的(原型对象:所有子对象集中保存共有属性值和方法的父对象)

构造函数中都有一个自带的属性prototype,指向自己配对的原型对象。构造函数.prototype 如何继承:(此处承接上面new的第二件事)new的第二步自动让新创建的子对象,继承构造函数的原型对象。new的第二步自动设置子对象的__proto___ 属性,指向构造函数的原型对象。

// 添加共有属性
function Obj(a) {
    this.a = a;
    this.b = b;
    this.fn = function(){
        console.log(this.b)
    }
}
Obj.prototype.fn = function(){} // 直接给构造函数的prototype(即给原形对象直接添加方法强制赋值)
var obja = new Obj(1)
obja.__proto__ == Obj.prototype // true 子对象指向父对象的prototype

原型对象中的this

  • 判断this,只看在哪里如何调用!不要看定义在哪
  • 因为原型对象中的共有方法将来使用”子对象.方法名()”调用。
  • 原型对象中的this->将来调用这个公共函数的**.(点)**前的那个子对象。
  • 简单记: 谁调用指谁
function Student(name,age){
    this.name = name;
    this.age = age;
}
Student.prototype.intro = function(){
    console.log(this.name, this.age)
}
var zs = new Student('zs', 18)
var ls = new Student('ls', 19)
zs.intro() // ('zs', 18)
ls.intro() // ('ls', 19)

this指向第三种:原型对象中共有方法的this->将来调用共有方法的 .(点) 前的子对象

这里还可扩展 原型链:由多级父对象逐级继承形成的链式结构,此处暂不多讲,this指向可参考这篇文章 this指向的几种情况

3.多态

定义:同一个函数在不同情况下表现出不同的状态

  • a. 重载overload: 同一个函数,输入不同的参数,执行不同的逻辑
  • b. 重写overwrite: (推翻、遮挡) 从父对象继承来的成员不好用,都在子对象中重写同名成员
var ww = new Student('ww',20)
ww.intro = function () {
    console.log('overwrite', this.name, this.age)
}
ww.intro() // (overwrite, ww, 19)

2.JS创建对象的10种方式总结

上面介绍了三种

1. 直接量 var obj = {}

2. new Object()

3. 构造函数 function Obj(){}

4. 工厂函数方式

// 本质还是Object(),将来无法根据对象的原型对象准确判断对象的类型
function createObj(name,age){
    var obj = new Object()
    obj.name = name;
    obj.age = age;
    obj.intro = function(){  console.log(this.name,this.age) }
    return obj;
}
console.log('obj: ', createObj('ty',18));

5. 原形对象方式

// 先创建完全相同的对象,再给子对象添加个性化属性,这个步骤比较繁琐,重复在写prototype
function Obj() { }
Obj.prototype.name = 'ty';
Obj.prototype.age = 18;
Obj.prototype.intro = function () { console.log(this.name, this.age) };
var o1 = new Obj();
o1.name = 'cj' // 禁止修改共有属性,这里会自动创建 o1 自由的属性 name
console.log('o1: ', o1);
var o2 = new Obj();
o2.name = 'wd' // 同上
console.log('o2: ', o2);

6. 混合模式(构造函数+原型对象)

// 此方式不符合面向对象封装的思想
function Obj(name, age) {
    this.name = name;
    this.age = age;
}
Obj.prototype.intro = function () {
    console.log(this.name, this.age) 
};
var o1 = new Obj('ty', 18);
console.log(o1);

7. 动态混合

// 先创建完全相同的对象,再给子对象添加个性化属性。
// 缺点: 针对第6点不符合封装,改造的动态混合虽然符合封装,但语义不符,其实if只在创建第一个对象时有意义。
function Obj(name, age) {
    this.name = name;
    this.age = age;
    if (Obj.prototype.intro === undefined) {
        Obj.prototype.intro = function () {
            console.log(this.name, this.age)
        };
    }
}
var o1 = new Obj('ty', 18);
o1.intro();

8. 寄生构造函数

// 构造函数里调用其他的构造函数,这种方式可读性较差
function Obj(name, age){}
function Obj2(name, age, height) {
    var o = new Obj(name, age);
    o.height = height;
    return o
}
var o1 = new Obj('ty', 18, 180);
console.log('o1: ', o1);

9. 稳妥构造函数:闭包

// 优点:不用this,不用new!安全,可靠
// 缺点:闭包容易造成内存泄漏
function Obj(name, age) {
    var o = {
        getName: function () { return name },
        setName: function (val) { name = val },
        getAge: function () { return age }
    }
    return o
}
var o1 = new Obj('ty', 18);
console.log('o1: ', o1);

简单判断闭包形成 前端面试基础-面向对象

10. ES6 Class

前端面试基础-ES6 中的第七节Class

3.JS实现继承的7种方式

function Fruit(name) { // 定义一个父类型构造函数
    this.name = name;
    this.play = [1,2,3]
}
Fruit.prototype.intro = function (color) { // 原型对象方法
    console.log(this.name + ' is ' + color)
}

1. 原型链式继承

将父类的实例作为子类的原型 缺点:

  • 此方式创建子类实例时无法向父类构造函数传参
  • 这是因为两个实例使用的是同一个原型对象,内存空间是共享的,改变s1play属性,会发现s2也跟着发生变化了
function Apple(name){}
Apple.prototype = new Fruit()
Apple.prototype.name = 'apple'; // 只能继续在原型上添加参数

var s1 = new Apple(); 
var s2 = new Apple(); 
s1.play.push(4); 
console.log(s1.play, s2.play); // [1,2,3,4]

2. 构造函数继承

借助 call 调用Parent函数,可传参

缺点:可以看到,父类原型对象中一旦存在父类之前自己定义的方法 intro(),那么子类将无法继承这些方法

相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法

function Apple(name, color) {
    Fruit.call(this, name) // 将子的this替换父类的this
    this.color = color
}
var apple = new Apple('apple', 'red')
console.log(apple.name); // 没问题
console.log(apple.intro()); // 会报错

3. 实例继承

function Apple(name, color) {
    var o = new Fruit(name); // 创建子类型实例
    o.color = color
    return o;
}
var apple = new Apple('apple', 'red')

4. 拷贝继承

纯拷贝则无法获取父类,不可for in遍历的方法,没有继承关系(感觉不能叫继承)

function Apple(name, color) {
    var o = new Fruit(name); // 创建子类型实例
    for (const k in o) { 
        Apple.prototype[k] = o[k] // 一个个拷贝到子类的 prototype 这种就没有继承关系了
    }
    this.color = color
}
var apple = new Apple('apple', 'red')

5. 组合继承

是原型链继承与构造函数继承的组合

需要 使用 Fruit.call(this, name)Apple.prototype = new Fruit() 执行两次,造成了多构造一次的性能开销,所以有了下面的寄生组合继承

function Apple(name, color) {
    Fruit.call(this, name)
    this.color = color
}
Apple.prototype = new Fruit();
Apple.prototype.constructor  = Apple
var apple = new Apple('apple', 'red')

6. 寄生组合继承

组合继承的改良版本

  • 写法1
function Apple(name, color) {
    Fruit.call(this, name)
    this.color = color
}
(function () {
    var AppleNew = function () { } // 创建一个空的构造函数
    AppleNew.prototype = Fruit.prototype; // 将实例的 prototype 作为子类的原型
    Apple.prototype = new AppleNew()
})();

Apple.prototype.constructor = Apple
var apple = new Apple('apple', 'red')
  • 写法2
function Apple(name, color) {
    Fruit.call(this, name)
    this.color = color
}
Apple.prototype = Object.create(Fruit.prototype) // 更简洁
Apple.prototype.constructor = Apple
var apple = new Apple('apple', 'red')

7. ES6 Class 继承

前端面试基础-ES6 中的第七节Class

4.JS实现深克隆

首先实现浅克隆步骤:

    1. 创建一个空对象
    1. 遍历对象中的所有属性
    1. 每遍历一个属性就为新对象添加同名的新属性,属性值也相同
    1. 返回处理好的对象
var obj = { a: 1, b: 2, c: 3 }
console.log('obj: ', obj);
function shallowClone(obj) {
    var newObj = {}
    for (const key in obj) {
        newObj[key] = obj[key]
    }
    return newObj
}
var newObj = shallowClone(obj)
console.log('newObj: ', newObj);

浅克隆:是只复制对象的第一级属性值。如果对象的第一级属性中又包含引用类型,则只复制地址。 若需要克隆的对象包含引用类型的属性值,克隆后,新旧对象公用一个引用类型的属性值。任意一方修改了引用类型的内容,则另一方也会受到影响。 所以需要进行深克隆 深克隆:着引用类型的属性值一起克隆,使新旧双方都有自己的引用类型,则哪一方修改都再无瓜葛

实现深克隆的3种方式:

  • 1.JSON.stringify() + JSON.parse()
var obj = { a: 1, b: 2, c: 3 }
var strObj = JSON.stringify(obj)
var newObj = JSON.parse(strObj) // 复制的新对象

缺点:无法深克隆undefined值和内嵌函数

    1. Object.assign(target, source)
var obj = { a: 1, b: 2, c: 3 }
var newObj = Object.assign({}, obj) // 复制的新对象

缺点:当对象中只有一级属性,没有二级属性的时,此方法为深拷贝。 但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝。

    1. 自定义递归克隆
var obj = { a: 1, b: 2, c: 3 }
function deepClone(obj) {
    var newObj = {} // 创建一个空对象,准备接新的副本
    if (typeof obj === 'object') {
        if (obj === null) {
            newObj = null;
        } else if (Array.isArray(obj)) { // 数组对象
            newObj = [];
            for (const key in obj) { // 递归克隆数组中的每一项
                newObj.push(deepClone(obj[key]))
            }
        } else if (obj.constructor === RegExp) { // 正则表达式对象,直接赋值
            newObj = obj
        } else { // 普通对象,直接循环遍历复制对象中的每个属性值
            newObj = {}
            for (const key in obj) {
                newObj[key] = deepClone(obj[key])
            }
        }
    } else {
        newObj = obj;
    }
    return newObj;
}
console.log(deepClone(obj))

最后

以上的方式总结只是自己学习总结,有其他方式欢迎各位大佬评论 渣渣一个,欢迎各路大神多多指正,不求赞,只求监督指正( ̄. ̄) 有关该文章经常被面试问到的可以帮忙留下言,小弟也能补充完善完善一起交流学习,感谢各位大佬(~ ̄▽ ̄)~