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

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

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

如果一个对象有push和pop方法,并且提供了正确的实现,他就可以被当作栈来使用。 如果一个对象有length属性,也可以依照下标来存取属性(最好拥有slice和splice等方法),这个对象就可以被当作数组来使用

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

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

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 换句话说,多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而 消除这些条件分支语句。

“在电影的拍摄现场,当导演喊出“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+']'
}
}
复制代码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值