JS 高频笔试题——面向对象

301 阅读9分钟

面向对象 三步/三大特点

封装、继承、多态

封装

1、封装就是创建一个对象,集中保存现实中一个事物的属性和功能。 
2、将零散的数据封装进对象结构中,极其便于大量数据的管理维护。 
3、只要使用面向对象思想开发时,第一步都是先封装各种各样的对象结构备用

封装对象的3种方式:

1、{}
2new Object()
3、构造函数

JS 语言底层最核心的原理: JS 中所有对象底层都是关联数组

1、存储结构: 都是名值对儿的组合
2、访问成员时: 标准写法都是: 对象名/数组名["成员名"];简写都是: 对象名/数组名.成员名
3、强行给不存在的位置赋值,不但不会报错,而且还会自动添加该属性
4、强行访问不存在的位置的值,都不会报错,而是返回undefined5、都可以用for in循环遍历

new 做了4件事:

1、创建一个新的空对象等待
2、自动让新创建的子对象,继承构造函数的原型对象
3、调用构造函数: 
    3.1、将构造函数中的this->new刚创建的新对象
    3.2、在构造函数内通过强行赋值方式,为新对象添加规定的属性和方法
4、返回新对象的地址,保存到=左边的变量里。

继承

js中继承都是通过原型对象实现。

原型对象:替所有子对象集中保存共有属性值和方法的父对象。

内置类型:ES标准中规定的、浏览器已经实现、我们可以直接使用的类型。

String, Number, Boolean
Array, Date, RegExp
Math(不是类型,已经是一个{}对象) 
Error 
Function, Object
global(全局作用域对象,在浏览器中被window代替)
Symbol, BigInt

每种类型一定有2部分组成:

1、构造函数: 负责创建该类型的子对象
2、原型对象: 负责为该类型所有子对象集中保存共有的属性值和方法定义

JavaScript 标准内置对象

原型链: 由多级父对象逐级继承形成的链式结构

多态

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

1、重载overload: 同一个函数,输入不同的参数,执行不同的逻辑
2、重写override: (推翻、遮挡)

所有对象调用 toString 结果都竟然不一样?

1、所有数组家孩子调用toString(),调用的都是数组原型对象爸爸定义的好用的toString()
2、不再用Object爷爷的不好用的toString()
3、Date家也是如此!

总结:

1、封装: 创建对象,2种: 
    如果只创建一个对象: {} 
    如果反复创建多个相同结构的对象: 构造函数 
2、继承: 所有子对象共用的属性值和方法,都要放在构造函数的原型对象中 
3、多态: 重写: 只要觉得从父对象继承来的成员不要用,都在子对象中重写同名成员

this 共有几种情况

1、obj.fun() fun中的this->.前的obj对象 
2、new Fun() Fun中的this->new创建的新对象
3、原型对象中共有方法里的this->将来调用这个共有方法的.前的那个子对象
4fun()、匿名函数自调和回调函数中的 this->window
5、button.onclick=function(){} 或 button.addEventListener(“click”,function(){…})
DOM事件处理函数里的this->当前正在触发事件的.前的DOM元素对象。
(这里不能改成箭头函数! 一旦改为箭头函数,this指外层的window)
6、Vue中this默认都指当前vue对象
7、箭头函数中的this->当前函数之外最近的作用域中的this
   (箭头函数底层 相当于.bind())
8、可用call或apply,临时替换一次函数中的this;
   可用bind,永久替换函数中的this
   

总结: 谁调用就指谁。判断this,一定不要看在哪里定义,一定只看将来在哪里,如何被调用。

替换this的3种情况

1、一次调用函数时, 临时替换一次this

要调用的函数.call(替换this的对象, 实参值1,...)

调用call做3件事儿:

1、立刻调用一次.前的函数 
2、自动将.前的函数中的this替换为指定的新对象 
3、还能向要调用的函数中传实参值

2、如果多个实参值是放在一个数组中给的。 需要既替换this,又要拆散数组再传参

要调用的函数.apply( 替换this的对象 , 包含实参值的数组 )

调用apply也做3件事儿:

1、调用.前的函数 
2、替换.前的函数中的this为指定对象 
3、先拆散数组为多个元素值,再分别传给函数的形参变量

3、创建函数副本,并永久绑定this

bind() 不但可以提前永久绑定this,而且还能提前永久绑定部分实参值。

var 新函数=原函数.bind(替换this的对象, 不变的实参值) •

调用bind也做3件事儿:

1、创建一模一样的新函数副本
2、永久替换this为指定对象
3、永久替换部分形参变量为固定的实参值!

被bind()永久绑定的this,即使用call,也无法再替换为其它对象了。(箭头函数的底层原理)

总结:

1、只在一次调用函数时,临时替换一次this: call 
2、既要替换一次this,又要拆散数组再传参: apply 
3、创建新函数副本,并永久绑定this: bind

JS 中创建对象共有几种方式

1、new Object() 
缺点: 步骤多
2、字面量: var 对象名={} 
缺点: 如果反复创建多个对象,代码会很冗余
3、工厂函数方式: var obj = createObj(参数1,参数2....)
缺点: 本质还是Object(),将来无法根据对象的原型对象准确判断对象的类型
4、构造函数方式:var obj=new ObjName(参数1,参数2...)
缺点: 如果构造函数中包含方法,则重复创建,浪费内存
5、原型对象方式:先创建完全相同的对象,再给子对象添加个性化属性。
缺点: 步骤繁琐!
6、混合模式:先创建完全相同的对象,再给子对象添加个性化属性。
缺点: 不符合面向对象封装的思想。
7、动态混合:先创建完全相同的对象,再给子对象添加个性化属性。
缺点: 语义不符,其实if只在创建第一个对象时有意义。
8、寄生构造函数:构造函数里调用其他的构造函数。
缺点: 可读性差。
9、ES6 Class
10、稳妥构造函数:闭包,不用this,不用new!安全,可靠。
缺点: 使用了闭包,容易造成内存泄漏。

JS 中实现继承共有几种方式

1、原型链式继承: 将父类的实例作为子类的原型
缺点: 创建子类实例时,无法向父类构造函数传参
2、构造函数继承
3、实例继承
4、拷贝继承: 无法获取父类,不可for in遍历的方法
5、组合继承
6、寄生组合继承
7ES6 class extends继承

实现深克隆共有几种方式

浅克隆:只复制对象的第一级属性值。如果对象的第一级属性中又包含引用类型,则只复制地址。

浅克隆的问题:如果对象中又包含引用类型的属性值,则导致克隆后,新旧对象依然共用同一个引用类型的对象属性值。

结果: 任意一方修改了引用类型的对象内容,都会导致另一方同时受影响。

深克隆:不但复制对象的第一级属性值,而且,即使对象中又包含引用类型的属性值,深克隆也会继续复制内嵌类型的属性值。

结果: 克隆后,两个对象彻底再无瓜葛。

1JSON.stringify()以及JSON.parse(),无法深克隆undefined值和内嵌函数
2Object.assign(target, source)
3、自定义递归克隆函数
// 自定义递归克隆函数
function deepClone(target) {
    let newObj; // 定义一个变量,准备接新副本对象
    // 如果当前需要深拷贝的是一个引用类型对象
    if (typeof target === "object") {
        if (Array.isArray(target)) {
            // 如果是一个数组
            newObj = []; // 将newObj赋值为一个数组,并遍历
            for (let i in target) {
                // 递归克隆数组中的每一项
                newObj.push(deepClone(target[i]));
            }
            // 判断如果当前的值是null;直接赋值为null
        } else if (target === null) {
            newObj = null;
            // 判断如果当前的值是一个正则表达式对象,直接赋值
        } else if (target.constructor === RegExp) {
            newObj = target;
        } else {
            // 否则是普通对象,直接for in循环递归遍历复制对象中每个属性值
            newObj = {};
            for (let i in target) {
                newObj[i] = deepClone(target[i]);
            }
        }
        // 如果不是对象而是原始数据类型,那么直接赋值
    } else {
        newObj = target;
    }
    // 返回最终结果
    return newObj;
}

判断一个对象是不是数组类型的几种方法

1、obj.__proto__===Array.prototype
2Object.getPrototypeOf(obj)===Array.prototype
3Array.prototype.isPrototypeOf(obj)
4、obj.constructor===Array
5、obj instanceof Array
6Object.prototype.toString.call(obj)==='[object Array]'
7Array.isArray(obj)

关于面向对象的面试题

第一题

var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };

console.log(a);
console.log(b);
console.log(JSON.stringify(b));
a.n = 3;
console.log(b);
console.log(JSON.stringify(b));
console.log(a);

第二题

var a = {};
var b = {
    key: "a",
};
var c = {
    key: "c",
};
a[b] = "123";
a[c] = "456";

console.log(a[b]);

// 解析重点
// a["[object Object]"] = "123"
// a["[object Object]"] = "456"

第三题

function Foo() {
    Foo.a = function () {
        console.log(1);
    };
    this.a = function () {
        console.log(2);
    };
}
Foo.prototype.a = function () {
    console.log(3);
};
Foo.a = function () {
    console.log(4);
};
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();

// 函数也是对象

第四题

var x = 0;
var foo = {
    x: 1,
    bar: function () {
        console.log(this.x);
        var that = this;
        return function () {
            console.log(this.x);
            console.log(that.x);
        };
    },
};
foo.bar();
foo.bar()();

第五题

function A() {}
function B() {
    return new A();
}
A.prototype = new A();
B.prototype = new B();
var a = new A();
var b = new B();

console.log(a.__proto__ === b.__proto__);
console.log(a.__proto__, b.__proto__);

// 构造函数内部,如果return一个引用类型对象,则整个构造函数失效,而是返回这个引用类型的对象

第六题

function Foo() {
    getName = function () {
        console.log(1);
    };
    return this;
}
Foo.getName = function () {
    console.log(2);
};
Foo.prototype.getName = function () {
    console.log(3);
};
var getName = function () {
    console.log(4);
};
function getName() {
    console.log(5);
}

Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

// 声明提前 function整体提前,var 变量提前
// 函数也是对象
// new 函数名()
// 任何函数都可以当做构造函数被new调用,且任何函数都有原型对象prototype属性,只不过,大部分函数不是标准的构造函数内容而已