Description
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.call
或Function.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.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() ); // 输出: 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
。
使用call
或apply
借用其他对象的方法
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)
}
}