前端设计模式学习笔记(面向对象JavaScript, this、call和apply, 闭包和高阶函数)

JavaScript经过原型委托的方式来实现对象与对象之间的继承。 编程语言可分为两大类:一类是静态类型语言,另外一类是动态类型语言html

JavaScript是一门动态类型语言 鸭子类型的概念(若是它走起来像鸭子,叫起来也是鸭子,那么它就是鸭子) 鸭子类型知道咱们只关注对象的行为,而不关注对象的自己(关注HAS-A,而不是IS-A) 面向接口编程,而不是面向实现编程node

若是一个对象有push和pop方法,而且提供了正确的实现,他就能够被看成栈来使用。 若是一个对象有length属性,也能够依照下标来存取属性(最好拥有slice和splice等方法),这个对象就能够被看成数组来使用ajax

多态:同一操做做用于不一样的对象上面,能够产生不一样的解释和不一样的执行结果。换句话说,给不一样的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不一样的反馈。编程

多肽的思想是将"作什么"和"谁去作以及怎样去作"分离开来,也就是将"不变的事物"与"可能改变的事物"分离开来。动物都会叫,这是不变的,可是不一样类型的动物具体怎么叫是可变的。把不变的部分隔离出来,把可变的部分封装起来,这给予咱们扩展程序的的能力。设计模式

var makeSound = function( animal ) {
animal.sound()
}
var Duck = function(){}
Duck.prototype.sound = function(){
console.log('嘎嘎嘎嘎嘎')
}
var Chicken = function(){}
Chicken.prototype.sound = function(){
console.log('咯咯咯')
}
makeSound(new Duck())
makeSound(new Chicken())
复制代码

静态类型的面向对象语言一般被设计为能够向上转型:当给一个类变量 赋值时,这个变量的类型既可使用这个类自己,也可使用这个类的超类。这就像咱们在描述 天上的一只麻雀或者一只喜鹊时,一般说“一只麻雀在飞”或者“一只喜鹊在飞”。但若是想忽 略它们的具体类型,那么也能够说“一只鸟在飞”。数组

使用继承获得多态效果 JavaScript 的多态浏览器

多态的思想就是把“作什么”和“谁去作”分离开来,先要消除类型之间的耦合关系。若是类型之间的耦合关系没有被消除,那么咱们在makeSound 方法中指定了发出叫声的对象是,它就不可能再被替换成另一个类型。 JavaScript对象的多态性是与生俱来的缓存

多态的最根本好处在于,你没必要再向对象询问“你是什么类型”然后根据获得的答 案调用对象的某个行为——你只管调用该行为就是了,其余的一切多态机制都会为你安 排稳当。 7 换句话说,多态最根本的做用就是经过把过程化的条件分支语句转化为对象的多态性,从而 消除这些条件分支语句。bash

“在电影的拍摄现场,当导演喊出“action”时,主角开始背台词,照明师负责打灯 光,后面的群众演员伪装中枪倒地,道具师往镜头里撒上雪花。在获得同一个消息时, 每一个对象都知道本身应该作什么。若是不利用对象的多态性,而是用面向过程的方式来 编写这一段代码,那么至关于在电影开始拍摄以后,导演每次都要走到每一个人的面前, 确认它们的职业分工(类型),而后告诉他们要作什么。若是映射到程序中,那么程序 中将充斥着条件分支语句。”闭包

利用对象的多态性,导演在发布消息时,就没必要考虑各个对象接到消息后应该作什么。对象应该作什么并非临时决定的,而是已经事先约定和排练完比的。每一个对象应该作什么,已经成为了该对象的一个方法,被安装在对象的内部,每一个对象负责他们本身的行为。因此这些对象能够根据俄同一个消息,有条不紊分别进行各自的工做。 将行为分布在各个对象中,并让这些对象各自负责本身的行为,这正是面向对象设计的优势。

var googleMap = {
show: function(){
console.log("开始渲染谷歌地图")
}
}
var renderMap = function(){
googleMap.show();
}
renderMap()
复制代码

对象的多态性提示咱们,"作什么"和"怎么去作"是能够分开的。

var renderMap = function(map) {
if(map.show instanceof Function ) {
map.show();
}
}
renderMap(googleMap);
renderMap(baiduMap);

var sosoMap = {
show: function(){
console.log("开始渲染搜搜地图")
}
}
renderMap(sosoMap)
复制代码

在JavaScript这种将函数做为一等对象的语言中,函数自己也是对象,函数用来封装行为而且可以被四处传递。当咱们对一些函数发出“调用”的消息时,这些函数会返回不一样的执行结果,这是“多态”的一种体现,也是不少设计模式在JavaScript中能够用高阶函数代替实现的缘由

封装 封装数据 封装的目的是将信息隐藏。封装数据和封装实现,封装类型和封装变化 除了ECMAScript6中提供的let以外,通常咱们经过函数来建立做用域

var myObject = (function(){
var _name = 'sven'; // 私有变量
return {
getName: function(){  // 公开(public)
return _name
}
}
})
复制代码

封装实现封装类型和封装变化 原型模式和基于原型继承的JavaScript对象系统 在以类为中心的面向对象编程语言中,类和对象的关系能够想象成铸剑模和铸件的关系,对象老是从类中建立而来。而在原型编程思想中,类并非必需的,对象未必须要从类中建立而来,一个对象是经过克隆另一个对象所获得的。 原型模式的实现关键,是语言自己是都提供了clone方法。ECMAScript 5提供了Object.create方法,能够用来克隆对象。

var Plane = function(){
this.blood = 100;
this.attackLevel = 1;
this.defenseLevel = 1;
} //建立构造函数
var plane = new Plane() //经过new建立一个实例
plane.blood = 500;
plane.attackLevel = 10;
plane.defenseLevel = 7;

var clonePlane = Object.create(plane)
复制代码

在不支持Object.creaate方法的浏览器中,则可使用如下代码

Object.create = Object.create || function (obj) {
var F = function(){}
F.prototype = obj
return new F();
}
复制代码

克隆是建立对象的手段

JavaScript自己是一门基于原型的面向对象语言,它的对象系统就是使用原型模式来搭建的,在这里称之为原型编程范型也许更合适。 原型编程范型的一些规则 若是A对象是从B对象克隆来的,那么B对象就是A对象的原型。 Object 是 Animal 的原型,而 Animal 是 Dog 的原型,它们之间造成了一 条原型链。这个原型链是颇有用处的,当咱们尝试调用 Dog 对象的某个方法时,而它自己却没有 这个方法,那么 Dog 对象会把这个请求委托给它的原型 Animal 对象,若是 Animal 对象也没有这 个属性,那么请求会顺着原型链继续被委托给 Animal 对象的原型 Object 对象,这样一来便能得 到继承的效果,看起来就像 Animal 是 Dog 的“父类”,Object 是 Animal 的“父类”。

基于原型链的委托机制就是原型继 承的本质。

如今咱们明白了原型编程中的一个重要特性,即当对象没法响应某个请求时,会把该请求委 托给它本身的原型。 最后整理一下本节的描述,咱们能够发现原型编程范型至少包括如下基本规则。

  1. 全部的数据都是对象。
  2. 要获得一个对象,不是经过实例化类,而是找到一个对象做为原型并克隆它。
  3. 对象会记住它的原型。
  4. 若是对象没法响应某个请求,它会把这个请求委托给它本身的构造器的原型。
function Person( name ){ this.name = name;
};
Person.prototype.getName = function(){ return this.name;
};
var a = new Person( 'sven' )
console.log( a.name ); // 输出:sven
console.log( a.getName() ); // 输出:sven
console.log( Object.getPrototypeOf( a ) === Person.prototype );
// 输出:true
复制代码

在这里 Person 并非类,而是函数构造器,JavaScript 的函数既能够做为普通函数被调用, 7 也能够做为构造器被调用。当使用 new 运算符来调用函数时,此时的函数就是一个构造器。 用 new 运算符来建立对象的过程,实际上也只是先克隆 Object.prototype 对象,再进行一些其余额 外操做的过程。

对象会记住它的原型

就JavaScript的真正实现来讲,其实并不能说对象有原型,而只能说对象的构造器有原型。对象把委托请求给它的构造器原型。 JavaScript给对象提供了一个__proto__的隐藏属性,某个对象的__proto__属性默认会指向它的构造器的原型对象,即{Constructor}.prototype。 实际上,__proto__就是对象跟"对象构造器的原型"联系起来的纽带。正由于对象要经过__proto__属性来记住它的构造器原型,因此咱们用objectFactory函数模拟用new建立对象时,须要手动给obj对象设置正确的__proto__指向。 obj.proto = Constructor.prototype; 经过这句代码,咱们让obj.__proto__指向Person.prototype,而不是原来的Object.prototype 若是对象没法响应某个请求,它会把这个请求委托给它的构造器的原型 虽然JavaScript的对象最初都是由Object.prototype对象克隆而来的,但对象构造器的原型并不只限于Object.prototype上,而是能够动态只想其余对象。当对象a须要借用对象b的能力时,能够有选择性地把对象a的构造器的原型只想对象b,从而达到继承的效果。

var obj = {name: 'sven'};
var A = function(){}

A.prototype = obj
var a = new A();
console.log(a.name)
复制代码

首先,尝试遍历对象 a 中的全部属性,但没有找到 name 这个属性。 查找 name 属性的这个请求被委托给对象 a 的构造器的原型,它被 a.proto 记录着而且指向 A.prototype,而 A.prototype 被设置为对象 obj。 在对象 obj 中找到了 name 属性,并返回它的值。

当咱们指望获得一个'类'继承自另外一个‘类’的效果时,每每会用下面的代码来模拟实现:

var A = function(){} //创造构造函数
A.prototype = {name: 'sven'};//构造函数的原型指向一个字面量对象

var B = function(){};
B.prototype = new A();// 将构造器的原型指向另一个对象,和指向字面量同样

var b = new B();
console.log(b.name)
复制代码

但美中不足是在当前的 JavaScript 引擎下,经过 Object.create 来建立对象的效率并不高,通 常比经过构造函数建立对象要慢。此外还有一些值得注意的地方,好比经过设置构造器的 prototype 来实现原型继承的时候,除了根对象 Object.prototype 自己以外,任何对象都会有一个 原型。而经过 Object.create( null )能够建立出没有原型的对象。 另外,ECMAScript 6 带来了新的 Class 语法。这让 JavaScript 看起来像是一门基于类的语言, 但其背后还是经过原型机制来建立对象。经过 Class 建立对象的一段简单示例代码1以下所示 :

Class Animal {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
Class Dog extends Animal {
constructor(name) {
super(name)
}
speck(){
return 'woof'
}
}
var dog = new Dog("Scamp")
console.log(dog.getName() + 'says' + dog.speak())
复制代码

this的指向

除去不经常使用的 with 和 eval 的状况,具体到实际应用中,this 的指向大体能够分为如下 4 种。  做为对象的方法调用。

当函数做为对象的方法被调用时,this指向该对象:

var obj = {
a:1,
getA: function(){
alert(this === obj) // 输出:true
alert(this.a)  // 输出:1
}
}
复制代码

 做为普通函数调用。

当函数做为普通函数调用时,this老是指向全局对象。

window.name = 'globalName'
var myObject = {
name: 'sven',
getName: function(){
return this.name
}
}

var getName = myObject.getName;将对象的方法赋给全局变量
console.log(getName()) // globalName
复制代码

有时候咱们会遇到一些困扰,好比在 div 节点的事件函数内部,有一个局部的 callback 方法, callback 被做为普通函数调用时,callback 内部的 this 指向了 window,但咱们每每是想让它指向 该 div 节点,见以下代码:

<html> <body>
<div id="div1">我是一个 div</div> </body>
<script>
    window.id = 'window';

document.getElementById( 'div1' ).onclick = function(){ alert ( this.id ); // 输出:'div1'
var callback = function(){
alert ( this.id );
        callback();
    };

</script> </html>
// 输出:'window'
}
复制代码

此时有一种简单的解决方案,能够用一个变量保存 div 节点的引用: 图灵社区会员 轩辕 专享 尊重版权 26

第 2 章 this、call 和 apply

document.getElementById( 'div1' ).onclick = function(){ var that = this; // 保存 div 的引用
var callback = function(){
alert ( that.id ); // 输出:'div1' }
callback(); };
在 ECMAScript 5 的 strict 模式下,这种状况下的 this 已经被规定为不会指向全局对象,而 是 undefined:
function func(){ "use strict"
alert ( this ); func();
复制代码

 构造器调用。

JavaScript 中没有类,可是能够从构造器中建立对象,同时也提供了new运算符,使得构造器更像一个类 除了宿主提供的一些内置函数,大部分JavaScript函数均可以看成构造器使用。构造器的外表跟普通函数如出一辙,它们的区别在于被调用的方式。当用new运算符调用函数时,该函数总会返回一个对象,一般状况下,构造器的this就指向返回的这个对象。 但用 new 调用构造器时,还要注意一个问题,若是构造器显式地返回了一个 object 类型的对象,那么这次运算结果最终会返回这个对象,而不是咱们以前期待的 this:  Function.prototype.call 或 Function.prototype.apply 调用。 跟普通的函数调用相比,用Function.prototype.call或Function.prototype.apply能够动态地改变传入函数的this:

var obj1 = {
name: 'sven',
getName: function(){
return this.name;
}
}
var obj2 = {
name: 'anne'
}
console.log(obj1.getName.call(obj2)) //输出:anne

丢失的this
var obj = {
myName: 'sven',
getName: function(){ return this.myName;
} };
console.log( obj.getName() );
var getName2 = obj.getName; console.log( getName2() );
// 输出:'sven' // 输出:undefined
复制代码

当调用 obj.getName 时,getName 方法是做为 obj 对象的属性被调用的,根据 2.1.1 节提到的规 律,此时的 this 指向 obj 对象,因此 obj.getName()输出'sven'。 当用另一个变量 getName2 来引用 obj.getName,而且调用 getName2 时,根据 2.1.2 节提到的 规律,此时是普通函数调用方式,this 是指向全局 window 的,因此程序的执行结果是 undefined。

闭包的更多做用

1.封装变量

闭包能够帮助把一些不须要暴露在全局的变量封装成"私有变量"。

var mult = function(){
var	a = 1;
for(var i = 0,l = arguments.length;i < l; i++){
a = a* arguments[i]
}
return a;
}
复制代码

加入缓存机制来提升这个函数的性能

var cache = {}
var mult = function(){
var args = Array.prototypr.join.call(arguments, ',')
if(cache[args]){
return cache[args]
}

var a = 1;
for(var i = 0, l = arguments.length; i < l; i++){
a = a* arguments[i]
}
return cache[args] = a
}

alert(mult(1,2,3)) ;//输出6
alert(mult(1,2,3)) ;//输出6
复制代码

优化全局变量cache

var mult = (function(){
var cache = {}
return function(){
var args = Array.prototype.join.call(arguments, ',')
if (args in cache) {
return cache[args]
}
var a = 1;
for(var i = 0, l = arguments.length; i<l; i++){
a = a* arguments[i]
}
return cache[args] = a
}
})()
复制代码

提炼函数是代码重构中的一种常见技巧。 若是在一个大函数中有一些代码可以独立出来,咱们经常把这些代码块封装在独立的小函数里面。独立出来的小函数有助于代码复用,若是这些小函数有一个良好的命名,它们自己也起到了注释的做用。 若是这些小函数不须要在程序的其余地方使用,最好是把它们用闭包封闭起来。

2.延续局部变量的寿命

img 对象常常用于进行数据上报。

var report = function(src) {
var img = new Image();
img.src = src
}

report('http://xx.com/getUserInfo')
复制代码

img是report函数的局部变量,当report函数的调用结束后,img局部变量随机被销毁,而此时或许还没来得及发出HTTP请求,因此这次请求就会丢失掉。把img变量用闭包封闭起来,便能解决请求丢失的问题。

var report = (function(){
var imgs = []
return function( src ){
var img = new Image()
imgs.push(img)
img.src = src
}
})()
report('http://xx.com/getUserInfo')
复制代码

当退出函数后,局部变量 imgs 并无消失,而是彷佛一直在某个地方 存活着。这是由于当执行 var f = func();时,f 返回了一个匿名函数的引用,它能够访问到 func() 被调用时产生的环境,而局部变量 a 一直处在这个环境里。既然局部变量所在的环境还能被外界 访问,这个局部变量就有了不被销毁的理由。在这里产生了一个闭包结构,局部变量的生命看起 来被延续了。

闭包和面向对象设计

过程与数据的结合是形容面向对象中的“对象”时常用的表达。对象以方法的形式包含了过程,而闭包则是在过程当中以环境的形式包含了数据。一般用面向对象思想能实现的功能,用闭包也能实现。反之亦然。

var extent = function(){
var value = 0;
return {
call: function(){
value++;
console.log(value)
}
}
}
var extent = extent()
extent.call();   // 1
extent.call();   // 2
extent.call();   // 3

var extent = {
value: 0,
call: function(){
this.value++;
console.log(this.value)
}
}
extent.call();   // 1
extent.call();   // 2
extent.call();   // 3
复制代码

或者:

var Extent = function() {
this.value = 0;
}

extent.prototype.call = function(){
this.value ++
console.log(this.value)
}
var extent = new Extent();
extent.call();   // 1
extent.call();   // 2
extent.call();   // 3
复制代码

用闭包实现命令模式

面向对象模的方式

<html>
<body>
<button id="execute">点击我执行命令</button>
<button id="undo">点击我执行命令</button>
</body>
</html>
<script>
var Tv = {
open: function(){
console.log("打开电视机")
},
close: function(){
console.log("关闭电视机")
}
}

var OpenTvCommand = function(receiver){
this.receiver = receiver;
}
OpenTvCommand .prototype.undo = function(){
this.receiver.close()
}
OpenTvCommand .prototype.execute = function(){
this.receiver.open()
}

var setCommand = function(command){
document.getElementById('execute').onclick = function(){
command.execute()
}
document.getElementById('undo').onclick = function(){
command.undo()
}
}

setCommand(new OpenTvCommand(Tv))
</script>
复制代码

命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之 间的耦合关系。在命令被执行以前,能够预先往命令对象中植入命令的接收者。 但在 JavaScript 中,函数做为一等对象,自己就能够四处传递,用函数对象而不是普通对象 来封装请求显得更加简单和天然。若是须要往函数对象中预先植入命令的接收者,那么闭包能够 完成这个工做。在面向对象版本的命令模式中,预先植入的命令接收者被当成对象的属性保存起 来;而在闭包版本的命令模式中,命令接收者会被封闭在闭包造成的环境中,代码以下

闭包形式

var Tv  = {
open: function(){
console.log('打开电视机')
},
close:function(){
console.log('关上电视机')
}
}

var createCommand = function(receiver) {
var excute = function(){
return receiver.open()
}
var undo = function(){
reutn receiver.close()
}

return {
execute: execute,
undo: undo
}
}
var setCommand = function( command ){
document.getElementById( 'execute' ).onclick = function(){
command.execute(); // 输出:打开电视机 }
document.getElementById( 'undo' ).onclick = function(){ command.undo(); // 输出:关闭电视机
} };
setCommand( createCommand( Tv ) );

复制代码

高阶函数

高阶函数是至少知足下列条件之一的函数。

1.函数能够做为参数被传递;

1.回调函数

var getUserInfo = function(userId, callback){
$.ajax('http://xxx.com/getUserInfo?' + userId,  funtion(data){
if(typeof callback === 'function'){
callback(data)
}
})
}
getUserInfo(13157, function(data){
alert(data.userName)
})

var appendDiv = function(){
for(var i = 0; i < 100; i++){
var div = document.createElement('div')
div.innerHTML = i
document.body.appendChild(div)
div.style.display = 'none'
}
}
appendDiv()
复制代码

转化为可复用的函数

var appendDiv = function(callback){
for (var i = 0; i< 100; i++){
var div = document.createElement('div')
div.innerHtml = i
document.body.appendChild(div)
if(typeof callback === 'function'){
callback(div)
}
}
}
appendDiv(function(node){
node.style.display = 'none'
})
复制代码

能够看到,隐藏节点的请求其实是由客户发起的,可是客户并不知道节点什么 时候会建立好,因而把隐藏节点的逻辑放在回调函数中,“委托”给 appendDiv 方法。appendDiv 方法当 然知道节点何时建立好,因此在节点建立好的时候,appendDiv 会执行以前客户传入的回 调函数。

Array.prototype.sort
复制代码

Array.prototype.sort 接受一个函数看成参数,这个函数里面封装了数组元素的排序规则。从 Array.prototype.sort 的使用能够看到,咱们的目的是对数组进行排序,这是不变的部分;而使 用什么规则去排序,则是可变的部分。把可变的部分封装在函数参数里,动态传入 Array.prototype.sort,使 Array.prototype.sort 方法成为了一个很是灵活的方法 //从小到大排列

[1,4,3].sort(function(a, b){
return a - b
})
复制代码

2.函数能够做为返回值输出。

1.判断数据类型

var isString = function(obj){
return Object.prototype.toString.call(obj) === '[object String]'
}
var isArray = function(obj){
return Object.prototype.toString.call(obj) === '[object Array]'
}
var isNumber = function(obj){
return Object.prototype.toString.call(obj) === '[object Number]'
}

var isTyoe = function(type){
return function(obj){
return Object.prototype.toString.call(obj) === '[object'+type+']'
}
}
复制代码
相关文章
相关标签/搜索