美文网首页
重剑无锋,大巧不工:JavaScript面向对象

重剑无锋,大巧不工:JavaScript面向对象

作者: 码农架构 | 来源:发表于2020-11-18 13:29 被阅读0次

    首先,我们将通过面向对象的三大特征,结合实例,介绍 JavaScript 面向对象的知识:封装、继承以及多态。

    封装

    在面向对象编程中,封装(Encapsulation)说的是一种通过接口抽象将具体实现包装并隐藏起来的方法。具体来说,封装的机制包括两大部分:

    限制对对象内部组件直接访问的机制;将数据和方法绑定起来,对外提供方法,从而改变对象状态的机制。

    直到 ES6(ECMAScript 6)以前,类(class)这个概念在 JavaScript 中其实不存在,但是 JavaScript 对函数(function)有着比一般静态语言强大得多的支持,我们经常利用它来模拟类的概念。

    Book() 函数 以看成 Book 类的构造函数.

    function Book(name) {
        this.name = name;
    }
    console.log(new Book("Life").name);
    

    实现属性私有:

    function Book(name) {
        this.getName = () => {
            return name;
        };
        this.setName = (newName) => {
            name = newName;
        };
    }
    let book = new Book("Life");
    book.setName("Time");
    console.log(book.getName()); // Time
    console.log(book.name); // 无法访问私有属性 name 的值
    

    上面的代码中,有两处变化,一个是使用了 () => {} 这样的语法代替了 function 关键字,使得其定义看起来更加简洁,但是表达的含义依然是函数定义,没有区别;第二个是增加了 getName() 和 setName() 这样的存取方法,并且利用闭包的特性,将 name 封装在 Book 类的对象中,你无法通过任何其它方法访问到私有属性 name 的值。

    继承

    在面向对象编程中,继承(Inheritance)指的是一个对象或者类能够自动保持另一个对象或者类的实现的一种机制。我们经常讲的子类具备父类的所有特性,只是继承中的一种,叫做类继承;其实还有另一种,对象继承,这种继承只需要对象,不需要类。

    在 ES6 以前,没有继承(extends)关键字,JavaScript 最常见的继承方式叫做原型链继承。原型(prototype)是 JavaScript 函数的一个内置属性,指向另外的一个对象,而那个对象的所有属性和方法,都会被这个函数的所有实例自动继承。

    function Base(name) {
        this.name = name;
    }
    function Child(name) {
        this.name = name;
    }
    Child.prototype = new Base();
    
    var c = new Child("Life");
    console.log(c.name); // "Life"
    console.log(c instanceof Base); // true
    console.log(c instanceof Child); // true
    
    • 设置 prototype 的语句一定要放到 Base 和 Child 两个构造器之外;
    • 并且要放在实例化任何子类之前。

    原型链继承有一个解决不了的问题:

    • 父类的构造方法如果包含参数,就无法被完美地继承下来。

    比如上例中的 name 构造参数,传入后赋值给对象的操作不得不在子类中重做了一遍。于是,我们引出另一种常见的 JavaScript 实现继承的方式——构造继承

    function Base1(name) {
        this.name = name;
    }
    function Base2(type) {
        this.type = type;
    }
    function Child(name, type) {
        Base1.call(this, name); // 让 this 去调用 Base1,并传入参数 name
        Base2.call(this, type);
    }
    
    var c = new Child("Life", "book");
    console.log(c.name); // "Life"
    console.log(c instanceof Base1); // false
    console.log(c instanceof Child); // true
    

    这种方法就能够保留父类对于构造器参数的处理逻辑,并且,我们居然还不知不觉地实现了多重继承!但是,缺点也很明显,使用 instanceof 方法判断的时候,发现子类对象 c 并非父类实例,并且,当父类的 prototype 还有额外属性和方法的时候,它们也无法通过构造继承被自动搬到子类里来。

    多态

    在面向对象编程中,多态(Polymorphism)指的是同样的接口,有着不同的实现。在 JavaScript 中没有用来表示接口的关键字,但是通过在不同实现类中定义同名的方法,我们可以轻易做到多态的效果,即同名方法在不同的类中有不同的实现。而由于没有类型和参数的强约束,它的灵活性远大于 Java 等静态语言。

    理解对象创建

    在 Java 等多数静态语言中,是使用 new 关键字加基于类名的方法调用来创建对象,但是如果不使用 new 关键字,只使用基于类名的方法调用,则什么都不是,编译器直接报错。但是 JavaScript 不同,我们对于类的概念完全是通过强大的函数特性来实现的,先看下面这个容易混淆函数调用和对象创建的例子:

    function Book(name) {
        this.name = name;
        return this;
    }
    console.log(new Book("Life").name); // 输出 Life
    console.log(Book("Life").name); // 也输出 Life
    

    你看,在 Book() 中,我们最终返回了 this,这就让它变得模糊,这个 Book() 到底是类的定义,还是普通函数(方法)定义?

    • 代码中使用 this 关键字来给对象自己赋值,看起来 Book 应该是类,那么 Book() 其实就是类的构造器,而这个赋值是完成对象创建的一部分;
    • 可是它居然又有返回(return 语句),那么从这个角度看,Book 应该是普通函数定义,函数调用显式返回了一个对象。

    于是,我们从上述最下面的两行代码中看到,无论使用 new 来创建对象,还是不使用 new,把它当成普通方法调用,都能够获得对象 name 属性的值“Life”,因此看起来用不用 new 似乎没有区别嘛?

    其实不然,没有区别只是一个假象。JavaScript 是一个特别善于创造错觉的编程语言,有许多古怪无比“坑”等着你去踩,而这只是其中一个。我们要来进一步理解它,就必须去理解代码中的 this,众所周知 this 可以看做是对象对于它自己的引用,那么我们在执行上述两步操作时,this 分别是什么呢?

    function Book(name) {
        console.log(this);
        this.name = name;
        return this;
    }
    new Book("Life"); // 打印 Book {}
    Book("Life"); // 打印 Window { ... }
    window.Book("Life") // 打印 Window { ... }
    

    在这段代码中,我在 Book() 内部把 this 打印出来了。原来,在使用 new 的时候,this 是创建的对象自己;而在不使用 new 的时候,this 是浏览器的内置对象 window,并且,这个效果和使用 window 调用 Book() 是一样的。也就是说,当我们定义了一个“没有归属”的全局函数的时候,这个函数的默认宿主就是 window。

    扩展阅读

    公众号:码农架构

    相关文章

      网友评论

          本文标题:重剑无锋,大巧不工:JavaScript面向对象

          本文链接:https://www.haomeiwen.com/subject/hykciktx.html