美文网首页
深入学习 JavaScript —— 面向对象思想

深入学习 JavaScript —— 面向对象思想

作者: 欧飞红 | 来源:发表于2019-06-12 21:36 被阅读0次

    前言

    这一篇比较简单,就总结整理下 JS 中实现面向对象设计思想,尤其是使用类实现的方法。我们都知道,JS 的语言底层是不支持类的,在不同场合我也看到不要实现类的建议。遗憾的是,即使在项目开发中不应用类,但在阅读一些插件的源代码时,难免也要了解。

    面向对象思想

    在用 JS 实现面向对象——通常是模拟类的过程中,出现了各种奇淫巧计。其中一些方法很难避免与原型的知识点的联系。如果你对这个还不是很了解,推荐你阅读本专栏的另一篇文章:

    深入学习 JavaScript —— 原型

    通常我们习惯用类来实现这种思想。问题在于,JavaScript 中,es5 及之前没有类的概念,es6 后虽然出现了一系列关键字支持类的实现,但是它的本质仍然是原型。这也是为什么大家习惯用各种奇怪的技巧来实现。

    忽略 es6 的 class 关键字,我们关注 js 怎样巧妙地实现类,以及,是否存在更好的方法?

    构造函数

    首先最经典的方法就是构造函数法,看

    示例1.1

    function Person(name, age) {
      this.name = name || "empty";  // 公有属性
      this.age = age || 0;
    }
    Person.prototype = {
      show: function() {  // 共享方法
        console.log(this.name + ": " + this.age);
      }
      type: "Human";   // 共享属性
    }
    var me = new Person("Ben", 20);
    me.show();
    // Ben: 20
    me.type;
    // "Human"
    

    它们之间的关系大概是这样的:

    image

    构造函数方法的优点在于:通过tihs“动态作用域”和原型来区分私有与公有。假如你想添加私有变量,你可以通过词法域定义;想添加公有方法或者公有变量,可以在原型对象上定义。这种方法相当常用,包括 jQuery 在内的各种插件都是这么做的。

    假如我们现在还想实现继承,最简单的应该就是使用 apply 来实现继承。

    示例1.2

    // 接上部分示例1.1
    function Student(name, age, school) {
      Person.apply(this, arguments);  // 继承父构造函数
      this.school = school;
    }
    var she = new Student("Lynn", 20, "SYSU");
    she.type;
    // undefined
    

    可以看到,这种方法有很明显的缺点,父构造函数在原型上公有方法和公有属性都没有得到继承。所以,要实现更完美的继承,我们应该在原型上动刀。

    这里推荐阮一峰的文章,他在这个问题上步步为营,有详细的“推导”:

    Javascript面向对象编程(二):构造函数的继承

    在原型上动刀,具体的代码如下:

    示例1.3

    // 接上部分示例1.1
    function extend(Child, Parent) { // 继承封装函数
      var F = function(){};
      F.prototype = Parent.prototype;
      Child.prototype = new F();  // 利用空对象作中介,避免 Child 的原型对象影响 Parent
      Child.prototype.constructor = Child; // 纠正实例对象的 construtor
      Child.uber = Parent.prototype; // 在子对象上打开一条通道,可以直接调用父对象的方法
    }
    function Student(name, age, school) {
      Person.apply(this, arguments);  // 继承父构造函数
      this.school = school;
    }
    extend(Student, Person);
    var she = new Student("Lynn", 20, "SYSU");
    she.type;
    // "Human"
    

    Perfect~实现了类构造和继承。上面那个继承封装函数,有一个部分还比较难理解——new()。关于它起到的作用,可以看看这篇文章:JavaScript深入之new的模拟实现

    闭包实现

    这个方法同样来自阮一峰的一篇文章的第三种方法,Javascript定义类(class)的三种方法。文中他将这种方法命名为“极简主义法”,你也可以叫做“工厂方法”。名字其实不重要,重要的是这种方法有两个特点:1. 避免原型的使用;2. 闭包实现。

    假如你还不知道闭包是什么的话,推荐你看看我写的这篇:

    深入学习JavaScript —— 闭包(Closure)

    我也是搜了一下知乎,才知道业内有这么一句话:闭包是穷对象,对象是穷闭包。实在是有意思的观点,但同时也说明了闭包保存状态的特点可以用来实现面向对象。下面是示例:

    示例2.1

    function sharedProperty(child, parent, key) {  // 访问描述符
      Object.defineProperty(child, key, {
        get: function() {
          return parent[key];
        },
        set: function(val) {
          parent[key] = val;
        }
      })
    }
    var Person = {
      type: "Human",
      createNew: function(name, age) {
        var person = {};
        sharedProperty(person, Person, "type"); // 共享属性
        person.name = name;   // 公有属性
        age = age;                     // 私有属性,只能通过内置函数访问
        person.show = function() {
            console.log(this.name + ": " + age);
        }; 
        return person;
      }
    };
    var me = Person.createNew("Ben", 20);
    me.show();
    // Ben: 20
    me.type
    // "Human"
    

    相比原来构造函数法,闭包实现有这两个优点:1. 不用动原型;2. 可以使用私有变量。

    另外,相比阮一峰文中提到的方法,我这里多了一个 sharedProperty 方法,这个方法利用访问描述符,实现了类似于构造函数法中以原型为基础的共享属性——即你修改单个物体的共享属性,其它物体的也会对应修改。

    假如现在想要实现继承,可以直接调用父类的“构造方法”,如下:

    示例2.2

    // 接上部分示例2.1
    var Student = {
      createNew: function(name, age, school) {
        var student = Person.createNew(name, age);
        student.school = school;
        return student;
      }
    };
    var she = Student.createNew("Lynn", 20, "SYSU");
    she.show();
    // Lynn: 20
    she.type
    // "Human"
    

    可以看到,基本上都能继承了。但是共享属性 type 未能与父类同步(假如想要实现的话,还是需要用到访问描述符)。所以相比构造函数法,闭包实现的劣势在于不能很好地实现共享属性。

    对象关联

    这个是《你不知道的JavaScript》书中提到的。对象关联是一种委托理论,基于标准所给定方法 Object.create 而实现的。简单来说,对象关联不纠结用类实现面向对象,而是原型来实现委托。委托行为意味着某些对象在找不到属性或者方法引用时会把这个请求委托给另一个对象

    那么,对象关联相比原来的类有什么特点呢?先看书中提到的示例代码:

    示例3.1

    var Task = {
      setID: function(ID) {this.id = ID;},
      outputID: function() {console.log(this.id);}
    };
    
    var XYZ = Object.create(Task);
    
    XYZ.prepareTask = function(ID, Label) {
      this.setID(ID);
      this.label = Label;
    };
    
    XYZ.outputTaskDetails = function() {
      this.outputID();
      console.log(this.label);
    };
    

    从代码中,能看到这些特点

    1. 状态保存在委托者 XYZ,而不是委托目标 Task
    2. 避免 [[prototype]] 链的不同层级上使用相同的命名
    3. 触发 this 的隐式绑定规则以达到我们想要的委托调用效果

    需要特别注意的是,委托最好是在内部实现,而不是直接暴露出去。即不建议使用者直接调用 XYZ.setID(),而是封装一个方法 prepareTask 作为所提供的api。

    这里有一个小小的问题,标准所给定的 Object.create 是 es6 的实现,如果想在 es5 及之前实现相同的效果,可以加入这段 polyfill 代码:

    示例3.2

    if (!Object.create) {
      Object.create = function (o) {
        function F() {};
        F.prototype = o;
        return new F();
      };
    }
    

    该方法的缺点是,不能实现私有属性和私有方法,对象之间也不好共享数据(要实现的话得在原型上动刀了)。

    后记

    先解释一下,文章又鸽了不是没有原因的:)。这一块一开始以为很简单,抄一下书就好,没想到后面越扩展越多,反而书上的内容并不多。

    关于类和委托孰优孰劣,《你不知道的 JavaScript》书中倾向使用委托,避免模拟类;winter 在极客时间的专栏《重学前端》里认为,两者各有优劣。我个人是站 winter 的观点,毕竟许多插件还是用到了类(就算不用还是要了解的)。

    那么,全文到这里就结束了。笔者考试周快到了,这周尽量更完本学期最后一篇文章(是一定!!),就要进入考试状态了,时间跨度一个月。不知道到时候还有没有这种笔耕不辍的状态,所以觉得不错的一定要给个点赞啊~~

    相关文章

      网友评论

          本文标题:深入学习 JavaScript —— 面向对象思想

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