Skip to content

面向对象、闭包、高阶函数 #15

Open
@isNeilLin

Description

@isNeilLin

Chapter1 - 面向对象的Javascript

Javascript通过原型委托的方式实现继承。

动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。

鸭子类型(duck typing):“如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。”

var duck = {
    duckSinging: function(){
        console.log( '嘎嘎嘎' );
    }
};

var chicken = {
    duckSinging: function(){
        console.log( '嘎嘎嘎' );
}
};

var choir = [];    // 合唱团

var joinChoir = function( animal ){
    if ( animal && typeof animal.duckSinging === 'function' ){
        choir.push( animal );
        console.log( '恭喜加入合唱团' );
        console.log( '合唱团已有成员数量:' + choir.length );
    }
};

joinChoir( duck );    // 恭喜加入合唱团
joinChoir( chicken );    // 恭喜加入合唱团

多态: 同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。多态的思想实际上是把“做什么”和“谁去做”分离开来。要实现这一点,归根结底要先消除类型之间的耦合关系。

“JavaScript对象的多态性是与生俱来的: “JavaScript的变量类型在运行期是可变的。一个JavaScript对象,既可以表示Duck类型的对象,又可以表示Chicken类型的对象。某一种动物能否发出叫声,只取决于它有没有makeSound方法,而不取决于它是否是某种类型的对象,这里不存在任何程度上的类型耦合。”

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只能依赖变量的作用域来实现封装特性。
封装包括:封装数据封装实现、封装类型、封装变化

原型模式和基于原型继承的JavaScript对象系统

在以类为中心的面向对象编程语言中,对象总是从类中创建而来。而在原型编程的思想中,一个对象是通过克隆另一个对象得到的。

原型模式的实现关键是语言本身是否提供了clone方法。ECMAScript 5提供了Object.create方法可以用来克隆对象。

var Plane = function(){
     this.blood = 50;
     this.attackLevel = 1;
    this.defenseLevel = 1;
}
var plane = new Plane();
plane.blood = 500;
plane.attackLevel = 10;
plane.defenseLevel = 7;

var clonePlane = Object.create(plane);
console.log(clonePlane)  // 输出:Object {blood: 500, attackLevel: 10, defenseLevel: 7}

// 兼容IE8
Object.create = Object.create || function(obj){
    var F = function() {};
    F.prototype = obj;
    return new F();
}

原型模式的真正目的并非在于需要得到一个一模一样的对象,而是提供了一种便捷的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段。

原型编程范型至少包括以下基本规则

  • 所有的数据都是对象
  • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
  • 对象会记住原型
  • 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型。

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

JavaScript中的原型继承

JavaScript中的根对象是Object.prototype对象,Object.prototype对象是一个空的对象。我们在JavaScript遇到的每个对象,实际上都是从Object.prototype对象克隆而来的,Object.prototype对象就是它们的原型。

var obj1 = new Object();
var obj2 = {};

console.log( Object.getPrototypeOf( obj1 ) === Object.prototype );    // 输出:true
console.log( Object.getPrototypeOf( obj2 ) === Object.prototype );    // 输出:true

“在JavaScript语言里,我们并不需要关心克隆的细节,因为这是引擎内部负责实现的。我们所需要做的只是显式地调用var obj1 = new Object()或者var obj2 = {}。此时,引擎内部会从Object.prototype上面克隆一个对象出来,我们最终得到的就是这个对象。”

var Person = function(name){
    this.name = name;
}
Person.prototype.getName = function(){
    return this.name;
}
var p = new Person('sven');

console.log(p.name) // sven
console.log(p.getName) // sven
console.log(Object.getPrototypeOf(p) === Person.prototype) // true

就JavaScript的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。对于“对象把请求委托给它自己的原型”这句话,更好的说法是对象把请求委托给它的构造器的原型。

Javascript给对象提供了一个__proto__的隐藏属性,某个对象的__proto__属性会指向它的构造器的原型对象。即{Constructor}.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); // sven

当我们期望得到一个“类”继承自另外一个“类”的效果时,往往会用下面的代码来模拟实现

var A = function(){}
A.prototype = {name: 'sven'};
var B = function(){}
B.prototype = new A()
var b = new B();
console.log(b.name) // sven

ECMAScript 6带来了新的Class语法。这让JavaScript看起来像是一门基于类的语言,但其背后仍是通过原型机制来创建对象。

class Animal {
    constructor(name){
        this.name = name;
    }
    getName(){
    		return this.name;
    }
}

class Dog extends Animal {
	constructor(name){
		super(name)
	}
	speak(){
		return "woof";
	}
}

var dog = new Dog("Scan");
console.log(dog.getName()+" says: "+ dog.speak());

chapter2-this、call和apply

javascript的this总是指向一个对象。具体指向哪个对象是在运行时基于函数的执行环境动态绑定的。this的指向大致可以分为四种:

  • 作为对象的方法调用
  • 作为普通函数调用
  • 构造器调用
  • Function.prototype.callFunction.prototype.apply调用

作为对象的方法调用

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

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

作为普通函数调用

当函数不作为对象的属性被调用时,也就是我们常说的普通函数方式,此时的this总是指向全局对象。

在ECMAScript 5的strict模式下,这种情况下的this已经被规定为不会指向全局对象,而是undefined

function func(){
    "use strict"
    alert ( this );    // 输出:undefined
}

func();

构造器调用

构造器的外表跟普通函数一模一样,它们的区别在于被调用的方式。当用new运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的this就指向返回的这个对象。

如果构造器显式地返回了一个object类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的this。如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成这个问题。

var MyClass = function(){
	this.name = 'sven';
	return {
		name: 'anne'
	}
}
var obj = new MyClass();
console.log(obj.name); // 'anne'

Function.prototype.callFunction.prototype.apply调用

Function.prototype.callFunction.prototype.apply可以动态地改变传入函数的this.

var obj1 = {
    name: 'sven',
    getName: function(){
        return this.name;
    }
};

var obj2 = {
    name: 'anne'
};

console.log( obj1.getName() );     // 输出: sven
console.log( obj1.getName.call( obj2 ) );    // 输出:anne

call和apply

apply接受两个参数,第一个参数指定了函数体内this对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply方法把这个集合中的元素作为参数传递给被调用的函数

var func = function( a, b, c ){
    alert ( [ a, b, c ] );    // 输出 [ 1, 2, 3 ]
};
func.apply( null, [ 1, 2, 3 ] ); 

call传入的参数数量不固定,跟apply相同的是,第一个参数也是代表函数体内的this指向,从第二个参数开始往后,每个参数被依次传入函数

var func = function( a, b, c ){
    alert ( [ a, b, c ] );    // 输出 [ 1, 2, 3 ]
};

func.call( null, 1, 2, 3 );

当使用call或者apply的时候,如果我们传入的第一个参数为null,函数体内的this会指向默认的宿主对象。但如果是在严格模式下,函数体内的this还是为null

使用callapply借用其他对象的方法

Math.max.apply(null,[6,4,8,1,2,7])  // 8

call和apply的实际用途

改变this指向

var obj1 = {
    name: 'sven'
};

var obj2 = {
    name: 'anne'
};

window.name = 'window';

var getName = function(){
    alert ( this.name );
};

getName();    // 输出: window
getName.call( obj1 );    // 输出: sven
getName.call( obj2 );    // 输出: anne

Function.prototype.bind

模拟Function.prototype.bind的实现

Function.prototype.bind = function(){
	var self = this,
	context = [].shift.call(arguments),
	args = [].slice.call(arguments);
	return function(){
		return self.apply(context,[].concat.call(args, [].slice.call(arguments)))
	}
}

var obj = {
	name: 'sven'
}

var func = function(a,b,c,d){
	console.log(this.name)  // sven
	console.log([a,b,c,d]) // [1,2,3,4]
}.bind(obj,1,2);

func(3,4)

借用其他对象的方法

(function(){
    Array.prototype.push.call( arguments, 3 );
    console.log ( arguments );    // 输出[1,2,3]
})( 1, 2 );

闭包

封装变量

var mult = (function(){
	var cache = {};
	return function(){
		var args = [].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;
	}
})()

console.log(mult(1,2,3)) // 6
console.log(mult(1,2,3)) // 6

提炼函数是代码重构中的一种常见技巧。如果在一个大函数中有一些代码块能够独立出来,我们常常把这些代码块封装在独立的小函数里面。独立出来的小函数有助于代码复用,如果这些小函数有一个良好的命名,它们本身也起到了注释的作用。如果这些小函数不需要在程序的其他地方使用,最好是把它们用闭包封闭起来。

var mult = (function(){
	var cache = {};
	var calculate = function(){
		var a = 1;
		for(var i = 0, l = arguments.length; i < l ; i++ ){
			a = a* arguments[i];
		}
		return a;
	}
	return function(){
		var args = [].join.call(arguments,',');
		if(cache[args]){
			return cache[args]
		}
		return cache[args] = calculate.apply(null,arguments);
	}
})()

延续局部变量的寿命

使用img进行数据上报

var report = (function(){
	var imgs = [];
	return function(){
		var img = new Image();
		imgs.push(img);
		img.src = src;
	}
})()

面向对象实现命令模式

<html>
	<body>
		<button id="execute">execute</button>
		<button id="undo">undo</button>
		<script>
			var Tv = {
				open: function(){
					console.log('open tv');
				},
				close: function(){
					console.log('close tv');
				}
			}

			var OpenTvCommand = function(receiver){
				this.receiver = receiver;
			}
			OpenTvCommand.prototype.execute = function(){
				this.receiver.open();
			}
			OpenTvCommand.prototype.close = function(){
				this.receiver.close();
			}
			
			var setCommand = function(command){
				document.getElementById('execute').onclick = function(){
					command.execute();
				}
				document.getElementById('undo').onclick = function(){
					command.close();
				}
			}
			
			setCommand(new OpenTvCommand(Tv))
			
		</script>
	</body>
</html>

判断一个数据是否是数组,在以往的实现中,可以基于鸭子类型的概念来判断,比如判断这个数据有没有length属性,有没有sort方法或者slice方法等。但更好的方式是用Object.prototype.toString来计算。Object.prototype.toString.call( obj )返回一个字符串,比如Object.prototype.toString.call( [1,2,3] )总是返回"[object Array]",而Object.prototype.toString.call( "str")总是返回"[object String]"。

var isType = function(type){
	return function(obj){
		return Object.prototype.toString.call(obj) === '[object '+type+']';
	}
}

var isString = isType('String');
var isNumber = isType('Number');
var isArray = isType('Array');

console.log(isArray([1,2,3,4]));  // true

JavaScript实现AOP(面向切面编程)

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。

Function.prototype.before = function(beforeFn){
	var __self = this; 
	return function(){
		beforeFn.apply(this,arguments);
		return __self.apply(this,arguments);
	}
}

Function.prototype.after = function(afterFn){
	var __self = this; 
	return function(){
		var ret = __self.apply(this,arguments);
		afterFn.apply(this,arguments);
		return ret;
	}
}

var func = function(){
    console.log( 2 );
};

func = func.before(function(){
    console.log( 1 );
}).after(function(){
    console.log( 3 );
});**JavaScript实现AOP(面向切面编程)**

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。

```javascript
Function.prototype.before = function(beforeFn){
	var __self = this; 
	return function(){
		beforeFn.apply(this,arguments);
		return __self.apply(this,arguments);
	}
}

Function.prototype.after = function(afterFn){
	var __self = this; 
	return function(){
		var ret = __self.apply(this,arguments);
		afterFn.apply(this,arguments);
		return ret;
	}
}

var func = function(){
    console.log( 2 );
};



func();

函数柯里化

currying又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

var curring = function(fn){
	var args = [];
	return function(){
		if(arguments.length===0){
			fn.apply(this,args)
		}else{**JavaScript实现AOP(面向切面编程)**

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。

```javascript
Function.prototype.before = function(beforeFn){
	var __self = this; 
	return function(){
		beforeFn.apply(this,arguments);
		return __self.apply(this,arguments);
	}
}

Function.prototype.after = function(afterFn){
	var __self = this; 
	return function(){
		var ret = __self.apply(this,arguments);
		afterFn.apply(this,arguments);
		return ret;
	}
}

var func = function(){
    consol
			[].push.call(args,arguments);
			return arguments.callee;
		}
	}
}

var cost = (function(){
    var money = 0;

    return function(){
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            money += arguments[ i ];
        }
        return money;
    }

})();

var cost = currying( cost );
cost( 100 );    // 未真正求值
cost( 200 );    // 未真正求值
cost( 300 );    // 未真正求值

console.log( cost() );     // 求值并输出:600

反柯里化(uncurrying)的实现

Function.prototype.uncurrying = function(){
	var self = this; // self就是Array.prototype.push
	return function(){
		// Array.prototype.push当做Function.prototype.call的this传进去
		return Function.prototype.call.apply(self,arguments)
		// arguments是[obj,'first'],apply接受数组形式的参数
		// 接下来被直接传入Function.prototype.call,call接受以逗号分隔的N个参数的形式
		// 所以arguments中的第一个参数会被当做call里面的this,其他参数一次传入call
		// 整句代码相当于 Array.prototype.push.call(obj,'first');
	}
}

var push = Array.prototype.push.uncurrying();
var obj = {
	length: 1,
	a: 'test'
};
push( obj, ‘first’ );

函数节流

var throttle = function(fn,interval){
	var self = fn, 
		 timer, firstTime = true;
	return function(){
		var _this = this, args = arguments;
		if(firstTime){
			self.apply(_this, args)
			return firstTime = false;
		}
		if(timer){
			return false;
		}
		timer = setTimeout(function(){
			clearTimeout(timer);
			timer = null;
			self.apply(_this,args)
		},interval||500)
	}
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    JSJavaScript内容

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions