youngwind / blog

梁少峰的个人博客
4.66k stars 385 forks source link

thief系列之二:从获取DOM和增删类中看js如何构造一个类 #63

Open youngwind opened 8 years ago

youngwind commented 8 years ago

要解决的问题

上一节 #60 我们已经知道如何使用IIFE和闭包来处理模块化的问题。现在我们来尝试实现两个功能。

  1. 获取DOM元素
  2. 添加删除类

我们回想一下,jquery完成上面的功能大概是这样的。

$('selector');
$('selector').addClass('className');
$('selector').removeClass('className');

我们分析一下。

  1. $函数接受选择器作为参数,返回DOM元素。
  2. 返回的DOM元素拥有addClass和removeClass方法。

    工厂模式

ok,上最基础的工厂模式。

var T = function (selector) {

    // 获取DOM元素
    var dom = document.querySelector(selector);

    // 添加类
    dom.addClass = function (className) {
      this.classList.add(className);
    };

    // 删除类
    dom.removeClass = function (className) {
      this.classList.remove(className)
    };
    return dom;
  };

缺点

第一,返回的DOM元素无法知道它是从T这个构造函数中来的。 证据:

typeof T('h1') === T;
// return false;

你可能会说,你这不废话吗!这里只用了工厂模式,压根没构造模式,哪儿来的构造函数! 没错,不过我们看看jquery

typeof $('h1') === jQuery
// return true;

看见没?我们现在姑且可以认为jQuery那样做的好处在于起码我知道实例化的对象是从哪儿来的。

第二,每个dom实例的addClass和removeClass方法都会在内存中存一份,并不能共享,造成资源上的浪费。证明依据。

T('h2').addClass === T('h1').addClass
// return false

构造模式

我们先来尝试解决工厂模式的第一个缺点。

// 构造模式
  var T = function (selector) {

    // 获取DOM元素
    this.dom = document.querySelector(selector);

    // 添加类
    this.addClass = function (className) {
      this.dom.classList.add(className);
    };

    // 删除类
    this.removeClass = function (className) {
      this.dom.classList.remove(className);
    };
  };

验证

new T('h1') instanceof T
// return true;

发现第一个问题已经解决。我们来看第二个问题,函数复用问题。 我们把addClass和removeClass抽象成全局函数(注意,这里的全局是指自执行表达式里面的)

(function () {

  function addClass(className) {
    this.dom.classList.add(className);
  }

  function removeClass(className) {
    this.dom.classList.remove(className);
  }

  // 构造模式
  var T = function (selector) {

    // 获取DOM元素
    this.dom = document.querySelector(selector);

    // 添加类
    this.addClass = addClass;

    // 删除类
    this.removeClass = removeClass;
  };

  window.T = T;
})(window);

验证:

new T('h1').addClass === new T('h1').addClass
// return true;

缺点

addClass和removeClass被声明为全局。虽然我们通过闭包将这种影响限制在模块内,但是这样子做也总感觉失去了对象的封装性。

原型模式

为了解决构造模式的全局函数声明问题,我们使用prototype来实现所谓的原型模式 shit.....忽然发现光用原型模式居然没法搞出一个demo来。比如一般的原型模式是这样的。

function T(selector) {
  }

  T.prototype.dom = document.querySelector(selector);
  T.prototype.addClass = function (className) {
    this.dom.classList.add(className)
  };
  T.prototype.removeClass = function (className) {
    this.dom.classList.remove(className)
  };

实际上这是会报错的

Uncaught ReferenceError: selector is not defined

由此我们可以看到原型模式的一个重大缺陷:无法传递参数,也就是说每次构造出来的对象只能是一样的,这能忍? 当然,它还有另外一个重大缺陷,那就是传递引用。由于这里还不涉及到,就不说了。

混合模式

原型模式走不通,原因是因为不能传递参数。但是前面的构造函数可以传参啊,所以结合两者就是混合模式。

// 混合模式 = 构造模式 + 原型模式

  // 构造模式定义属性
  function T(selector) {
    this.dom = document.querySelector(selector);
  }

  // 原型模式定义方法
  T.prototype.addClass = function (className) {
    this.dom.classList.add(className)
  };

  T.prototype.removeClass = function (className) {
    this.dom.classList.remove(className)
  };

如何把new去掉?

目前我们已经做了很多了,但是还不够。比如,看下面的对比。

$('selector')
new T('selector')

jquery实例化的时候是不需要new关键字的,而我们的T因为包含构造模式,所以得使用new。那么jquery是如何把new去掉的呢?(当然,new $('selector')也是可以执行的) 这里有一篇文章讲得很不错,http://www.cnblogs.com/aaronjs/p/3278578.html 下面我直接给出改造过后的代码

function T(selector) {
    return new T.prototype.init(selector);
  }

  T.prototype = {
    init: function (selector) {
      this.dom = document.querySelector(selector);
      return this;
    },
    addClass: function (className) {
      this.dom.classList.add(className);
    },
    removeClass: function (className) {
      this.dom.classList.remove(className);
    }
  };

  T.prototype.init.prototype = T.prototype;