【JavaScript】(附面试题)一文搞懂面向对象编程

3,433 阅读6分钟

写在前面

关于面向对象,是前端必会的知识,同时它也是一种编程思想~掌握这块的知识,会对我们帮助非常大~

以前我写过几篇关于面向对象的文章,一篇实现new运算符,两篇读书笔记,一篇面试题,如下:

关于这篇文章,建议阅读之前写过的浏览器堆栈内存以及作用域和作用域链的文章:

今天这篇文章将从以下的顺序由浅入深理解面向对象

  • 面向对象编程概要

  • 原型、原型链的底层运行机制

  • 内置类原型拓展方法

  • 面试题

下面,开始正文~

面向对象编程概要

面向对象编程OOP(Object Oriented Programming)

一些定义

  • 对象:万物皆对象

  • 类:对对象的细分

  • 实例:类中具体的事物

JavaScript中,对实例、类和对象的划分如下:

实例 对象
1 Number Object
"1" String Object
[1, 2, 4] Array Object
true、false Boolean Object
null Null Object
Undefined Undefined Object
function() {} Function Object
{name: "1"} Object Object
/^$/ RegExp Object
Date Date Object
... ... ...

上面这些类都是JavaScript自身所拥有的,那么应该如何创建一个自定义类呢?创建自定义类的过程中都发生了什么?

自定义类

创建自定义类

function func() {
    this.num = 100;
}

func();      // 此种情况为普通函数执行,this指向window

new func();  // 此种情况new执行,就是一个自定义类

new函数执行的过程(加粗文字是与普通函数执行不同的地方)

  • 形成一个全新的执行上下文(EC)【每一次new都会形成一个新的实例对象】

  • 形成AO变量对象

  • 初始化作用域链

  • 默认创建一个对象,这个对象就是当前类的实例

  • 声明this指向新创建的实例

  • 代码执行

  • 不论是否有return,都会将新创建的实例返回

    • 如果有return,且返回值是一个引用类型值,就会返回return的值,如果不是引用类型值,就会返回创建的实例

    • 如果没有return,就会返回创建的实例

// 1.无return
function func() {
    this.num = 100;
}

let f = new func();

console.log(f);  // func {num: 100}

// 2.有return,且返回值为引用类型值
function func1() {
    let obj = {};
    obj.num = 10;
    this.num = 100;
    return obj;
}

let f1 = new func1();

console.log(f1);      // {num: 10}

// 3.有return,但是返回值不是引用类型值
function func2() {
    this.num = 100;
    return 1;
}

let f2 = new func2();

console.log(f2);      // func2 {num: 100}

其实在上面的例子中,返回的实例里面并不是只有num一个键值对,我们在 浏览器中的输出展开实例会发现,在其中还有一个__proto__

下面说一下prototype__proto__

原型、原型链的底层运行机制

原型和原型链就是对应上面所说的prototype__proto__,阅读此部分前我们可以想一下作用域和作用域链~

特点

  • 每一个类都具备prototype,并且属性值是一个对象

  • 对象上天生具备一个属性:constructor,指向类本身

  • 每一个对象(普通对象、prototype、实例...)都具备__proto__属性值是当前实例所属类的原型

  • __proto__的机制:先找私有属性,如果没有则开始找基于__proto__所属实例prototype上的公有属性,如果还是没有,则继续向上查找,一直到找到Object.prototype

接下来,看一道面试题,我们还是用绘图的方式进行理解:

function Fn() {
    this.x = 100;
    this.y = 200;
    this.getX = function () {
        console.log(this.x);
    }
}
Fn.prototype.getX = function () {
    console.log(this.x);
};
Fn.prototype.getY = function () {
    console.log(this.y);
};
let f1 = new Fn;
let f2 = new Fn;
console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);
console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
console.log(Fn.prototype.__proto__.constructor);
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();

上面的图里面,展示了这道题当中的几个关系,在其中,第六条中说,每一个__ptoto__都会指向当前实例所属类的原型,那么FnFn.ptototype__proto__指向了哪里?我们知道,函数所属的类是Object,那它们两个就是指向了Object,下面是示意图:

到这里,看这两张图,我们就可以知道上面的答案了

console.log(f1.getX === f2.getX);                       // false
console.log(f1.getY === f2.getY);                       // true
console.log(f1.__proto__.getY === Fn.prototype.getY);   // true
console.log(f1.__proto__.getX === f2.getX);             // false
console.log(f1.getX === Fn.prototype.getX);             // false
console.log(f1.constructor);                            // Fn() {...}
console.log(Fn.prototype.__proto__.constructor);        // Object() {...}
f1.getX();                                              // 100
f1.__proto__.getX();                                    // undefined
f2.getY();                                              // 200
Fn.prototype.getY();                                    // undefined

几种类的prototype、__proto__指向关系图

  • 函数

上面的题其实就是函数的指向关系图,只不过它还不完善,现在完善一下:

假如有一个函数Fn

function Fn() {
    ...
}

let f = new Fn();

  • 数组

现有一个数组arr

let  arr = [1, 2, 4];

我们根据这两张图再画出一张通用关系图

内置类原型拓展方法

内置类原型拓展方法意思就是在内置类的原型上添加方法或者修改现有方法,比如:

// 基于Array内置类拓展一个数组去重方法
Array.prototype.myFunc = function myFunc() {
    for(var i = 0; i < this.length; i++) {
        for(var j = i + 1; j < this.length; j++) {
            if(this[i] === this[j]) {
                this.splice(j, 1);
                j--;
            }
        }
    }
    return this;
}

var arr  = [1, 2, 4, 6, 8, 6, 4, 8];
console.log(arr.myFunc());     // [1, 2, 4, 6, 8]

var brr  = ["a", "b", "c", "b", "a", 1, 4, 8];
console.log(brr.myFunc());     // ["a", "b", "c", 1, 4, 8]

面试题

对于面试题,前面也有一篇文章,在文章开头也写出链接,点这里:【面试篇】几道面试题带你深入理解JavaScript面向对象进行查看

1、输出结果

function fun(){
    this.a = 0;
    this.b = function(){
        alert(this.a);
    }
}
fun.prototype = {
    b:function(){
        this.a = 20;
        alert(this.a);
    },
    c:function(){
        this.a = 30;
        alert(this.a)
    }
}
var my_fun = new fun();
my_fun.b();
my_fun.c();

根据图解,题目输出结果依次为:030

2、输出结果

function C1(name) {
    if (name) {
        this.name = name;
    }
}
function C2(name) {
    this.name = name;
}
function C3(name) {
    this.name = name || 'join';
}
C1.prototype.name = 'Tom';
C2.prototype.name = 'Tom';
C3.prototype.name = 'Tom';
alert((new C1().name) + (new C2().name) + (new C3().name));

/* *
 * new C1().name:没有传参,所以内部不执行,会顺着__proto__查找,最终找到"Tom"
 * new C2().name:没有传参,但是内部已经执行,name为undefined
 * new C3().name:没有传参,内部执行,name为join
 * 所以,最终结果为:"Tomundefinedjoin"
 */

3、a为什么,会输出"OK"

var a = ?;
if (a == 1 && a == 2 && a == 3) {
    console.log('OK');
}

==的机制:

  • null:null和任何值都不相等,包括自己

  • null 和 undefined:两个等号时为true,三个等号时为false

  • 对象 == 字符串:会将对象转换为字符串(调用对象.toString()方法)

  • 剩余的比较情况都将转换为数字(对象转换为数字:Number(对象.toString()))

了解机制后,我们回到题目本身,两次&&操作符,其实是相当于a比较了三次

了解这些后,我们有几种方法解决这个问题

第一种:

var a = {
    i: 0,
    toString() {
        return  ++this.i;
    }
}

if (a == 1 && a == 2 && a == 3) {
    console.log('OK');
}

第二种:

var a = [1, 2, 3];
a.toString = a.shift;

if (a == 1 && a == 2 && a == 3) {
    console.log('OK');
}

第三种:

var i = 0;

Object.defineProperty(window, "a", {
    get() {
        return ++i;
    }
})

if (a == 1 && a == 2 && a == 3) {
    console.log('OK');
}

解决问题的方法还有很多,此处只列出三种,欢迎有思路的小伙伴在评论区研究讨论~

最后

面向对象编程是贯穿整个前端学习当中的,掌握其基础以及更深层次的知识是必要的,也是必须的~觉得文章对你有帮助,可以给本篇文章点个赞呀~如果文章有不正确的地方,还希望大家指出~我们共同学习,共同进步~

最后,分享一下我的公众号「web前端日记」~大家可以关注一下~