美文网首页
JavaScript设计模式(一)

JavaScript设计模式(一)

作者: Mr君 | 来源:发表于2018-09-18 09:22 被阅读0次

静态方法

var CheckObject = function () {};
CheckObject.checkName = function () {}
CheckObject.checkEmail = function () {}
CheckObject.checkPassword = function () {}

var check = new CheckObject ();
console.log(CheckObject) // f() {}
console.log(check) // CheckObject {}
静态方法.png

这里我们可以看到,CheckObject直接声明并调用checkName、checkEmail、checkPassword方法,但是当其实例化以后,check中无法调用这些方法,其原因就是,这种方法定义的属性在一启动的时候已经被实例化,无法再次进行实例化,只能直接调用。这种无论如何实例化,都只创建一次的的方法或属性称之为静态方法或静态属性。

特点

  • 直接定义,直接调用
  • 静态方法在一启动的时候就被实例化了,不能再次实例化
  • 内存是连续的

实例方法

方法一:

var CheckObject = function () {
  this.checkName = function () {};
  this.checkEmail = function () {};
  this.checkPassword = function () {};
}

var check = new CheckObject ();

console.log(CheckObject);
console.log(check);
实例方法.png

方法二:

var CheckObject = function () {};
CheckObject.prototype.checkName = function () {};
CheckObject.prototype.checkEmail = function () {};
CheckObject.prototype.checkPassword = function () {};

var check = new CheckObject ();

console.log(CheckObject);
console.log(check);
实例方法2.png

这里可以看见,通过构造函数和原型对象中的方法都是可以被继承的。

构造函数方法与原型对象方法的区别
构造函数中添加的属性、方法是在当前对象上添加的,这类属性和方法在实例化的时候会被复制一次,而原型对象中的对象和属性在实例化时被所有实例化对象共用。

特点

  • 构造函数或原型链中定义,实例化后调用,相同属性构造函数中的优先级高于原型链
  • 程序运行过程中生成内存,是离散的空间
  • 静态方法无法访问实例成员变量,如下:
function user (a,n) {
  this.a = a || '年龄';
  this.n = n || '名字';
}

// 声明静态变量
user.sayname = function () {
  alert(this.a) 
}
// 这里声明后可以使用
user.sayname(); // undefined

// 声明动态变量
user.prototype.sayname2 = function () {
  alert(this.a);
}

// 实例方法需要实例化来引用
var o = new user();
o.sayname(); // '年龄'

提示

实例方法也可以写成如下形式:

var CheckObject = function () {};
CheckObject.prototype = {
  checkName: function () {},
  checkEmail: function () {},
  checkPassword: function () {}
}

但是两种方式不能混用,后者在为原型对象赋值新对象时,它将覆盖之前prototype对象赋值的方法。

注意下面这种情况

// 图书类
var Book = function (title, time, type) {
  this.title = title;
  this.time = time;
  this.type = type;
}

// 实例化一本书
var book = Book('JavaScript', '2014', 'js');

console.log(book); //undefined
console.log(window.title); // JavaScript
console.log(window.type); // js
未实例化.png

在上面的例子中,没有使用new关键字来实例化,所以会直接执行这个函数,而这个函数在全局作用域中执行了,所以在全局作用域中this指向当前对象就是全局变量,即window,添加的属性被添加到window上了,这个book变量的最终是要得到Book的执行结果,由于没有return语句,book的结果即为undefined。

链式调用

简单例子

var CheckObject = {
  checkName: function () {
    // 验证姓名
    return this;
  },
  checkEmail: function () {
    // 验证邮箱
    return this;
  },
  checkPassword: function () {
    // 验证密码
    return this;
  }
}

这时,我们可以这样使用

CheckObject.checkName().checkEmail().checkPassword();

在原型对象中

var CheckObject = function () {};
CheckObject.prototype = {
  checkName: function () {
    // 验证姓名
    return this;
  },
  checkEmail: function () {
    // 验证邮箱
    return this;
  },
  checkPassword: function () {
    // 验证密码
    return this;
  }
}

使用的时候需要创建

var a = new CheckObject ();
a.checkName().checkEmail().checkPassword();

链式添加&链式调用

首先,为了避免污染原生对象Function,造成不必要的开销,我们抽象出一个统一添加方法的功能方法:

Function.prototype.addMethod = function (name, fn) {
  this[name] = fn;
  return this;
}

// 使用
var methods = function () {};

// 链式添加声明
methods.addMethod('checkName', function () {
  //验证姓名
  return this;
}).addMethod('checkEmail', function () {
  // 验证邮箱
  return this;
})

// 链式调用
methods.checkName().checkEmail();

这里,我们换一种写法

Function.prototype.addMethod = function (name, fn) {
  this.prototype[name] = fn; // 改在原型上
  return this;
}

// 使用
var Methods = function () {};

// 链式添加声明
Methods.addMethod('checkName', function () {
  //验证姓名
  return this;
}).addMethod('checkEmail', function () {
  // 验证邮箱
  return this;
})

// 链式调用
// 这里需要注意,不能直接使用,因为方法被添加在其prototype属性上,需要用new关键字来创建新的对象
var m = new Methods();
m.checkName().checkEmail();

面向过程

面向过程最常见的写法就是编写一个个的函数来解决需求,如对三个输入框中输入的数据校验,分别编写三个函数,弊端就是会在页面中增加很多全局变量,不利于别人重复使用,一旦使用别人提供的方法,就不能轻易修改这些方法,不利于团队维护。

面向对象

面向对象是将需求抽象成一个对象,然后针对这些对象分析其特征(属性)与动作(方法),这个对象我们称之为类。面向对象的三个特点:封装、继承、多态。

封装

封装是指功能封装到对象中,这样能够带来极高的灵活性,使代码的编写更加自由。注意,一般将代表类的变量名的首字母大写,然后将这个类的内部通过对this变量添加属性或方法来实现对类添加属性或方法。
方法一:

var Book = function (id, bookname, price) {
  this.id = id;
  this.bookname = bookname;
  this.price = price;
}

方法二:

Book.prototype.display = function () {
};

方法三:

Book.prototype = {
  display: function () {}
};

这样,我们将需要的方法和属性封装在Book类中,在使用功能方法的时候不能直接使用Book类,需要实例化,创建新的对象,如:

var book = new Book(10, 'JavaScript模式', 50);

私有属性、私有方法 & 公有属性、公有方法

这些概念在JavaScript没有显性的存在,但是可以通过函数级作用域来创建类的私有属性私有方法,外界无法直接访问;利用this创建的属性和方法,在new关键字实例化时,this上定义的属性和方法可以复制到新创建的对象上可以通过外部访问的特性,来实现公有属性公有方法。这里通过this创建的方法不但可以访问到公有属性和公有方法,还可以访问到私有属性和私有方法,这些方法权限比较大,这里有可以看成是特权方法。在通过new关键字实例化对象的时候,特权方法会初始化对象的一些属性。因此,在这些在创建对象时调用的特权方法看成类的构造器

// 私有属性与私有方法,特权方法,对象公有属性和对象公有方法,构造器
var Book = function (id, name, price) {

  // 私有属性
  var num = 1;

  // 私有方法
  function checkId () {}

  // 特权方法
  this.getName = function () {
    return checkId
  };
  this.getPrice = function () {
    return num
  };

  // 对象公有属性
  this.id = id;

  // 对象公有方法
  this.copy = function () {};


  // 构造器
  this.setName = function () {};
  this.setPrice = function () {};
}

// 类静态共有属性(对象不能访问)
Book.isChinese = true;

// 类静态公有方法(对象不能访问)
Book.resetTime = function () {}

Book.prototype = {
  // 公有属性
  isJSBook: false,

  // 公有方法
  display: function () {}
}

在这里静态方法仍然不能被实例化,原型链和构造函数创造的方法和属性实例化后都可以被访问。

通过new关键字创建对象实质上是对新对象this的不断赋值,并将prototype指向父类的prototype 所指向的对象,即两个prototype指向的是同一个对象。


类.png

利用闭包来实现

通常,闭包是在函数内部创建另一个函数,这里,我们可以将闭包作为创建对象的构造函数,这样它既是闭包又是可访问到类函数作用域中的变量,如下bookNum变量。

// 利用闭包实现
var Book = (function () {
  // 静态私有变量
  var bookNum = 0;

  // 静态私有方法
  function checkBook (name) { };
  
  // 创建类
  function _book (newId, newName, newPrice) {
    // 私有变量
    var name, price;

    // 私有方法
    function checkId (id) {};

    // 特权方法
    this.getName = function () {};
    this.getPrice = function () {};
    this.setPrice = function () {};
    this.setName = function () {};

    // 公有属性
    this.id = newId;

    // 公有方法
    this.copy = function () {};
    bookNum++;
    if (bookNum > 100) {
      throw new Error ('我们仅出版100本书'); 
    }

    // 构造器
    this.setName(name);
    this.setPrice(price);
  }

  // 构建原型
  _book.prototype = {
    // 静态公有属性
    isJSBook: false,
  
    // 静态公有方法
    display: function () {}
  };
  return _book;
})();

使用的时候仍然需要new关键字

var book = new Book();

然而,对于初学者来说,这种写法很容易忘记写new,这里我们寻求一种安全模式,能够避免这种情况的产生:

// 图书安全类
var Book = function (title, time, type) {
  // 判断执行过程中this是否指向当前对象(如果是,说明是用new创建的)
  if (this instanceof Book) {
    this.title = title;
    this.time = time;
    this.type = type;
  } else { // 否则重新创建这个对象
    return new Book (title, time, type);
  }
}
var book = Book ('JavaScript', '2014', 'js');

// 测试
console.log(book); // Book
console.log(book.title); // JavaScript
console.log(book.time); // 2014
console.log(book.type); // js
console.log(window.title); // undefined
console.log(window.time); // undefined
console.log(window.type); // undefined

继承

每个类都有3个部分

  • 构造函数内,提供实例化对象复制用的
  • 构造函数外,直接通过点语法添加的,供类使用,实例化对象无法访问
  • 类的原型中,实例化对象通过其原型间接访问,为供所有实例化对象共用

JavaScript本身并没有继承机制,需要我们自己进行封装

  • 类式继承
// 声明父类
function SuperClass () {
  this.superValue = true;
}

// 为父类添加公有方法
SuperClass.prototype.getSuperValue = function () {
  return this.superValue;
};

// 声明子类
function SubClass () {
  this.subValue = false;
}

// 继承父类
SubClass.prototype = new SuperClass();

// 为子类添加公有方法
SubClass.prototype.getSubValue = function () {
  return this.subValue;
}

在这里我们可以看到,类式继承中需要将第一个类的实例赋值给第二个类的原型,这样可以使子类原型访问到父类的原型属性和方法以及父类构造函数中赋值的属性和方法。但是使用的时候需要注意,instanceof 用于判断前面的对象是否是后面类的实例,这并不表示两者的继承关系,SubClass.prototype继承了SuperClass:

var instance = new SubClass();
console.log(instance instanceof SuperClass); //true
console.log(instance instanceof SubClass); // true
console.log(SubClass instanceof SuperClass); // false
console.log(SubClass.prototype instanceof SuperClass); // true

类式继承的缺点

  • 类式继承过程中实例化父类,如果父类构造函数及其复杂,会造成不必要的开销。
  • 子类prototype是父类的实例化,继承了父类,当父类的公有属性是引用类型,就会在子类中被所有实例引用,因此一个子类实例更改子类原型从父类构造函数中继承来的公有属性就会直接影响其他子类,如:
function SuperClass () {
  this.books = ['JavaScript', 'html', 'css'];
}
function SubClass() {};
SubClass.prototype = new SuperClass();
var instance1 = new SubClass();
var instance2 = new SubClass();
console.log(instance2.books); // ['JavaScript', 'html', 'css']
instance1.books.push('设计模式');
console.log(instance2.books); // ['JavaScript', 'html', 'css', '设计模式']
  • 子类实现继承是靠其原型prototype对父类的实例化实现的,因此在创建父类的时候无法向父类传参,因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化,如图


    无法传参.png
  • 构造函数继承

//声明父类
function SuperClass (id) {
  // 引用类型共有属性
  this.books = ['JavaScript', 'html', 'css'];
  // 值类型共有属性
  this.id = id;
}

// 父类声明原型方法
SuperClass.prototype.showBooks = function () {
  console.log(this.books);
}

// 声明子类
function SubClass (id) {
  // 继承父类
  SuperClass.call(this,id)
}

// 创建第一个子类实例
var instance1 = new SubClass(10);

// 创建第二个子类实例
var instance2 = new SubClass(11);

instance1.books.push('设计模式');
console.log(instance1.books); // ['JavaScript', 'html', 'css', '设计模式']
console.log(instance1.id); // 10
console.log(instance2.books); ['JavaScript', 'html', 'css']
console.log(instance2.id); // 11

instance1.showBooks(); // TypeError

值得注意的是,这里用call方法改变了函数作用环境,因此,子类继承了父类用this声明的公有方法和属性,但是这种方法并没有涉及prototype,所以父类原型上的方法不会被子类继承,如果想要继承必须放在构造函数中,但是这种方法的弊端也就显示出来了,即每个创建出来的实例都会单独拥有一份属性和方法,而不能被共用。

  • 组合继承
    类式继承中通过子类原型prototype对父类实例化来实现;构造函数继承通过子类构造函数作用环境来制心一处父类构造函数来实现;组合继承兼具这两个特点:
// 声明父类
function SuperClass (name) {
  // 值类型共有属性
  this.name = name;
  this.books = ['html', 'css', 'JavaScript'];
}

// 父类原型共有方法
SuperClass.prototype.getName = function () {
  console.log(this.name);
}

// 声明子类
function SubClass (name, time) {
  // 构造函数式继承父类name属性
  SuperClass.call (this, name);
  // 子类中新增公有属性
  this.time = time;
}

// 类是继承 子类原型继承父类
SubClass.prototype = new SuperClass();

// 子类原型方法
SubClass.prototype.getTime = function () {
  console.log(this.time);
}

var instance1 = new SubClass('js book', 2014);
instance.books.push('设计模式');
console.log(instance1.books); // ['html', 'css', 'JavaScript', '设计模式']
instance1.getName(); // js book
instance1.getTime(); // 2014

var instance2 = new SubClass('css book', 2015);
console.log(instance2.books); // ['html', 'css', 'JavaScript']
instance2.getName(); // css book
instance2.getTime(); // 2015

这里可以看见,子类实例更改继承自父类的引用类型的属性,不会影响到其他实例,且子类实例化过程中又能将参数传递到父类构造函数中。
缺点就是父类构造函数调用了两次,在构造函数继承的时候执行了一次父类构造函数,在实现子类原型的类式继承的时候又调用了一次父类构造函数,开销较大。

  • 原型式继承
function inheritObject (o) {
  // 声明一个过渡函数对象
  function F () {};

  // 过渡对象的原型继承父对象
  F.prototype = o;

  // 返回过渡对象的一个实例,该实例的原型继承了父对象
  return new F();
}
原型式继承.png

这里可以看到,它相当于是对类式继承的一个封装,其中的过渡对象相当于类式继承中的子类,出现的目的是为了返回新的实例化对象,在这种方法中F过渡类的构造函数中没有内容,开销较小,使用起来更加方便,在ES5中新增了Object.create()方法,实现了原型式继承。
缺点就是子类只能继承父类中的属性和方法,而不能进行二次拓展,有属于自己的属性。

  • 寄生式继承
function createBook (obj) {
  // 通过原型继承方式创建新对象
  var o = new inheritObject(obj);
  // 拓展新对象
  o.getName = function (obj) {
    console.log(name);
  }

  // 返回拓展后的新对象
  return o;
}

这里我们看到,寄生式继承是在原型继承的二次封装过程中的拓展,新创建的对象不仅仅有父类的属性和方法,还可以添加新的属性和方法。
另外,有一个问题想求助于读者,在上面的函数中,

var o = new inheritObject (obj);

这一行,可以不用new关键字吗,因为inheritObject()函数本身返还的是new F()。下面是自己的实验


寄生式继承.png
寄生式继承对比.png

感觉不会影响继承本身的效果,如有反对意见,欢迎提出哈!

  • 寄生式组合继承
    由上面的例子可以看出,寄生式继承源于原型继承,而原型继承源于类式继承,在类式继承和构造函数继承组合形成的组合继承中子类,并不是父类的实例,子类的原型才是父类的实例。在这里我们将寄生式继承和构造函数继承组合成的寄生式组合继承。
function inheritPrototype (subClass, superClass) {
  // 复制一份父类的原型副本保存在变量中
  var p = inheritObject(superClass.prototype);

  // 修正因为重写子类导致的子类constructor属性被修改
  p.constructor = subClass;

  // 设置子类原型
  subClass.prototype = p;
}
// 声明父类
function SuperClass (name) {
  // 值类型共有属性
  this.name = name;
  this.books = ['html', 'css', 'JavaScript'];
}

// 父类原型共有方法
SuperClass.prototype.getName = function () {
  console.log(this.name);
}

// 声明子类
function SubClass (name, time) {
  // 构造函数式继承父类name属性
  SuperClass.call (this, name);
  // 子类中新增公有属性
  this.time = time;
}

// 寄生式继承父类原型
inheritPrototype (SubClass, SuperClass);

// 子类新增原型方法
SubClass.prototype.getTime = function () {
  console.log(this.time);
}

// 创建测试用例
var instance1 = new SubClass('js book', 2014);
var instance2 = new SubClass('css book', 2013);

instance1.books.push('ES6');
console.log(instance1.books); // ["html", "css", "JavaScript", "ES6"]
console.log(instance2.books);// ["html", "css", "JavaScript"]

instance2.getName(); // css book
instance2.getTime(); // 2013

多态

多态,即同一个方法多种调用,JavaScript中对传入的参数做判断,以实现多种调用方式,或者采用函数柯理化。

function add(num) {
  var sum = num;
  var tmp = function (v) {
    sum += v;
    return tmp;
  }
  tmp.toString = function () {
    return sum;
  }
}

console.log(add(10)(20)(50)); // 80

参考文献

张容铭 《JavaScript 设计模式》

相关文章

网友评论

      本文标题:JavaScript设计模式(一)

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