JavaScript: The Good Parts 读书笔记(二)

二.函数

  • JS 中函数亦是对象
          使用字面变量表面法产生的对象被链接到Object.prototype.而函数对象将会链接到 Function.prototype.(该原型对象本身也是连接到 Object.prototype).
          每个函数在创建时将会附有个隐藏属性: 函数的执行上下文(function context) 和 实现函数行为的代码(!).
    每个函数对象在创建时也随带有一个 prototype 属性,它的值是一个拥有constructor 属性的对象. 该constructor属性的值既是函数本身(!). 这和隐藏连接到的 Function.prototype 完全不同!!
          因为函数是对象,所以它们可以像任何其他的值一样被使用。函数可以存放在变量,对象和数组中。也可被看做参数传递给其他函数,函数自身也可返回另一个函数。最后,还是因为函数是对象,它也可以拥有属性(方法). 或许函数与普通对象最大的不同之处在于,它可以被调用(通过'(args)'进行). 
          函数可以通过字面表达来创建:
    var add = function(a,b){
    	return a + b;
    };
     
  • 函数的声明:
      函数字面表达包括四个部分:
         第一个部分是保留字 function.
         第二个部分是函数名,该名称可以被忽略. 如果名称被忽略,该函数将成为一个匿名函数. 函数可以用它的名称来递归调用自己。此名称也可以被调试器和开发工具识别.
         第三部分是包围在圆括号中的一组参数。其中每个参数用逗号分隔。这些名称将被定为函数中的变量(!),这些变量不像普通变量一样被初始化为undefined. 而是在该函数被调用时初始化为实际调用的参数值.
         第四部分是包括在{}内的函数体,函数体内的语句在调用时执行.

       调用一个函数将暂停当前函数的执行,传递控制权和参数给新函数。 除了声明时定义的形参, 每个函数接受两个附加的参数: thisarguments. 参数this的取值取决于被调用的模式.在JS中一共有四种调用模式: 方法调用模式,函数调用模式构造器调用模式Apply/Call调用模式.这些模式在如何初始化关键参数this 上存在着差异.

       函数调用时,使用圆括号来表达,圆括号中的可以包含用逗号分隔的表达式,每个表达式产生一个参数值。当实际参数的个数与形式参数的格式不匹配时并不会导致运行时错误,如果实际参数值过多了,超出的参数值将会被忽略(!). 如果实际参数值过少,缺少的值将会被替换为 undefined.

  • 方法调用模式
        当一个函数被保存为对象的一个属性时,我们称它为一个 方法. 当一个方法被调用时,this被绑定到该对象上。如果一个调用表达式包含一个属性存取表达式(即.或[]导航),那么他被看做一个方法调用模式.
    var myObject = {
    	value : 0,
    	increment : function(inc){
    		// 方法this可以访问对象的属性。 this到对象的绑定发生在调用时。
    		// 这种延迟绑定使得函数可以对this进行高度复用.
    		// 通过this可以访问所属对象上下文的方法称为公共方法(!).
    		this.value += typeof inc === "number" ? inc : 1;
    	}
    };
    
    myObject.increment(); // 使用属性存取表达式调用,属于方法调用模式.
    document.writeln(myObject.value);
    myObject.increment(2);
    document.writeln(myObject.value);
    
     
  • 函数调用模式
       当一个函数并非一个对象的属性时,那么它被当做一个函数来调用:
    var sum = add(3,4); // 7
       当函数以此模式调用时,this被绑定到全局对象。这是JS语言设计上的一个错误。在正确设计时,当函数中的内部函数被调用时,this应该仍然绑定到外部函数的this变量.这个设计错误的后果是方法不能利用内部函数来帮助它工作,因为内部函数的this被绑定了错误的值, 所以不能共享该方法对原对象的访问权。幸运的是,有一个很容 易的解决方案。通过定义一个临时变量.
    myObject.double = function(){
    	var that = this; // 保存外部函数的上下文.
    
    	var helper = function(){
    		that.value = add(that.value,that.value);
    	};
    
    	helper(); // 函数方式调用helper. 此时helper内部的context 是 window. 所以需要定义 that.
    };
    
    myObject.double();
    document.writeln(myObject.value);
     
  • 构造器调用模式
       Javascript 是一门基于原型继承的语言。 这意味着对象可以直接从其他对象继承属性。如果在一个函数前面带上 new 来调用,那么将创建一个隐示链接到该函数的prototype成员上的新对象,同时 this 将会被绑定到这个新对象上。
    // 按照约定,对于构造器变量. 它们需要以开头大写的格式来命名。
    var Quo = function(string){
    	this.status = string;
    };
    // 为所有Quo 的实例提供一个get_status方法.
    Quo.prototype.get_status = function(){
    	return this.status;
    };
    var myQuo = new Quo("confused");
    document.writeln(myQuo.get_status()); // 令人困惑的结果.
    
     
  • Apply/Call 调用模式
      因为JavaScript 是一门函数式的OO语言,所以函数自身也可以拥有方法.
    var args = [3,4];
    var sum = add.apply(null,args); // context 为null 时表示将context 绑定到 window.
    document.writeln(sum);
    
    var statusObject = {status : "A-OK"};
    // 使用statusObject 作为函数上下文去调用 get_status方法.
    var status = Quo.prototype.get_status.apply(statusObject);
    document.writeln(status);
    
       apply传入的是一个参数数组,也就是将多个参数组合成为一个数组传入,而call则作为call调用的参数传入(从第二个参数开始)。如 func.call(func1,var1,var2,var3)对应的apply写法为:func.apply(func1,[var1,var2,var3])使用apply的好处是可以直接将当前函数的arguments对象作为apply的第二个参数传入.

  • 函数的参数
       函数被调用时,另外一个传入的默认参数为 arguments 数组,通过它函数可以访问所以它被调用时传递给它的参数列表。因为语言设计的错误,arguments 并不是一个真正的数组(!), 他只是一个类似数字的对象,拥有一个 length属性,但他缺少所有的数组方法(!).
    var sum = function(){
    	var i,sum = 0;
    	for(i = 0 ; i < arguments.length; i++){
    		sum+=arguments[i];
    	}
    	return sum;
    };
    document.writeln(sum(4,8,15,16,23,42));// 108
    
     
  • 函数的返回值
        当一个函数被调用时,它从第一个语句开始执行,并在函数代码执行完后结束。 return 语句可以用来使函数提前返回. 需要注意的是,一个函数总是会返回一个值,如果没有指定返回值,默认返回 undefined.

  • 异常处理
        JS提供了一套异常处理机制。通过使用 throw object; 可以抛出一个异常.
    var checked_add = function(a,b){
    	if(typeof a !== 'number' || typeof b !== 'number'){
    		throw{
    			name : "TypeError",
    			message : "Add needs Numbbers!"
    		};
    	}
    	return add(a,b);
    };
    
    // 通过使用 catch(exception) 可以捕获抛出的异常信息.
    // 这里与强类型语言不同,一个try块只能存在一个捕获所有异常的catch块(!).
    var try_it = function(){
    	try{
    		checked_add('not number');
    	}catch(e){
    		document.writeln(e.name+":"+e.message);
    	}
    };
    
    try_it();
    
     
  • 扩展函数对象
        Javascript 允许给语言的基本类型增加方法。例如,我们可以给Function.prototype 增加方法来扩展所有函数对象.
    Function.prototype.method = function(name,func){
    	this.prototype[name] = func;
    	return this;
    };
    // 此时该扩展的method 方法对所有继承自改原型链的对象生效.
    // 该方法可以简化使用 prototype.XX = function(){} 来增加功能.
    
    // 简单类型可以使用 new Number(),new String() 等构造实例,
    // 表明这些对象也继承自原型链 Function.prototype.
    Number.method("integer",function(){ // 为所有数字增加一个取整方法.
    	return Math[this < 0 ? "ceiling" : "floor"](this);
    });
    document.writeln(( 10 / 3).integer()); // 3.3333 --> 3
    
    String.method("trim",function(){
    	return this.replace(/^\s+|\s+$/g,'');
    });
    document.writeln("  Hello World!  ".trim());
    
      
        通过给基本类型增加方法,我们可以大大提高语言的表现力。因为Javascript原型继承的动态本质。新的方法立刻被赋予到所有的值(对象实例)上,哪怕值(对象实例)是在方法被创建之前就已经实例化了。基本类型的原型是公共的结构,所以在类库混用时务必小心。一个保险的做法就是只在确定没有该方法时才添加它。所以上述方法可以写为:
    //  Function.prototype.method = function(name, func){
    //     if(!this.prototype[name]){
    //        this.prototype[name] = func;
    //     }
    //  }
    
     
  • 函数的递归
      与其他编程语言一样,JS中也提供了函数的递归处理.
    var walk_the_dom = function(node, callback){ // 自顶向下遍历DOM树
    	callback(node);
    	node = node.firstChild;
    	while(node){
    		walk_the_dom(node, callback);
    		node = node.nextSibling;
    	}
    };
    
    var getElementsByAttribute = function(attr,value){
    	var results = [];
    	walk_the_dom(document.body,function(node){
    		var actual = node.nodeType === 1 && node.getAttribute(attr);
    		if(typeof actual === 'string' 
                 && (actual === value || typeof value !== 'string')){
    			results.push(node);
    		}
    	});
    	return results;
    };
    
     
  • Javascript 的块作用域
       在其他编程语言中,块作用域控制着变量与参数的可见性及生命周期。该特性减少了名称冲突,并且提供了自动内存管理. 但在JS中并非如此.
    var foo = function(){
    	var a = 3, b = 5;
    	var bar = function(){
    		var b = 7; c = 11; // b 覆盖了外面 b = 5 的定义;(其他语言中不可以这样声明)
    		a+=b+c; // a += 7 + 11 --> 21. 因为闭包的关系,外面的 a 被赋值为 21.
    		window.alert(b); // 在该块内,b = 7;
    	};
    	
    	bar();
    	window.alert(b); // 在块外, b = 5;
    	window.alert(c); // 可以访问在块内声明的c
    };
    
    foo();
    
     
         JavaScript does have function scope. That means that the parameters and variables defined in a function are not visible outside of the function, and that a variable defined  anywhere within a function is visible everywhere within the function.
          许多现代语言都推荐尽可能迟地声明变量。但对于Javascript, 这是非常糟糕的建议,因为它缺少块作用域。所以,最好的做法是在函数体的顶部就声明所有可能用到的变量

  • 闭包
        JS作用域的好处是内部函数可以访问定义它们的外部函数中的参数和变量(this和arguments除外). 一个更有趣的情形是内部函数拥有比他的外部函数更长的生命周期。例如,我们为了保护对象的值不被非法更改,可以使用该方式:
    var myObject_protected = function(){
    	var value = 0;
    
    	// 这里通过一个函数的形式初始化了对象。由于函数作用域的关系,内部
    	// 函数依然可以访问 value的值(闭包).
    	return {
    		increment : function(inc){
    			value += typeof inc === 'number' ? inc : 1;
    		},
    		getValue : function(){
    			return value;
    		}
    	};
    };
    
    var myObject_p = new myObject_protected();
    myObject_p.increment(10);
    document.writeln(myObject_p.getValue());
  • 几个使用闭包的例子:
    var fade = function(node){
    	var level = 1;
    	var step = function(){
    		var hex = level.toString(16);
    		node.style.backgroundColor = "#FFFF"+hex+hex;
    		// 这里只是通过条件判断来避免死循环。
    		if(level < 15){
    			level += 1;
    			window.setTimeout(step,100);
    		}
    	};
    	window.setTimeout(step,100);
    };
    
    fade(document.body);
    
     
       使用闭包时,理解内部函数是直接访问外部函数的实际变量,而非复制一份新变量是非常重要的(!).下面是一个错误示范:
                 
    // var add_the_handlers = function(nodes){
    //   var i;
    //   for(i = 0 ; i < nodes.length ; i ++){
    //      nodes[i].onclick = function(e){
    //        //(!) 直接访问了外部变量i, 并不是复制一份i的实例。
    //        //所以随着i一直自增,最后弹出来的均是nodes.length
    //        alert(i); 
    //      }
    //   }
    // }
    
     矫正方法:
    // var add_the_handlers = function(nodes){
    //   var i;
    //   for(i = 0 ; i < nodes.length ; i ++){
    //      nodes[i].onclick = function(i){
    //			return function(e){ // 返回另一个匿名函数,但是该函数可以访问外面匿名函数构造时传入的 i 实例.
    //            alert(i);
    //          };
    //      }(i); // 立即调用匿名函数 function(i);
    //   }
    // }
    
     
    一个定时器示例:
    window.setTimeout(function(){
    	for(var i = 1 ; i <= 10 ; i++){
    		window.setTimeout(function(i){
    			return function(){
    				var hex = i.toString(16);
    				document.body.style.backgroundColor = "#FF"+hex+hex+"FF";
    			};
    		}(i), 100*i);
    	}
    },2000);
    
     
  • 模块模式
       我们可以使用函数和闭包来构造模块(Module)。模块是一个提供接口但却隐藏状态与实现的函数(对象).例如,假设我们想要给String 增加一个deentityify方法,该方法将搜寻字符串中的HTML实体并替换为相应的字符.为了进行转换,需要构造一个转换表,但是何处保存此转换表呢?如果放在全局变量中,将会导致灵活性下降,虽然可以放在该函数本身。但这样有运行时的损耗。因为该函数在每次被执行的时候该字面变量都会被求值以此。所以最理想的方式是将其放入一个闭包。
    String.method("deentityify",function(){
    	// 字符实体表,它存放在闭包中,不会在每次deentityify调用时被实例化.
    	var entity = {
    		quot : '"',
    		lt : '<',
    		gt : '>'
    	};
    
    	// 返回真正的deentityify 方法
    	return function(){
    		return this.replace(/&([^&;]+);/g, function(a,b){
    			var r = entity[b];
    			return typeof r === 'string' ? r : a;
    		});
    	};
    }());
    
    // 注意最后一行使用()运算立即调用了外层的匿名函数,该函数返回了真正的实现方法。
    document.writeln("&lt;&quot;&gt;".deentityify());
    
     
        模块模式利用了函数的作用域和闭包来创建绑定对象与私有成员的关联,在这个例子中,只有deentityify方法有权访问字符实体表这个数据对象。

        模块模式的一般形式是: 一个定义了私有变量和内部函数的函数对象,利用闭包创建可以访问私有变量和函数的特权函数。 最后返回这个特权函数,或者把它们保存到一个可以访问到的地方

        使用模块模式就可以摒弃全局变量的使用。它促进了信息隐藏和其他优秀的设计实践。对于应用程序的封装,或者构造其他单例对象,模块模式非常有效。例如,下面使用模块模式来构造一个序列号生产对象,其利用闭包保存了一个外部不可修改的序列数:
    var serial_marker = function(){
    	var prefix = '';
    	var seq = 0;
    	return {
    		set_prefix : function(p){
    			prefix = p;
    		},
    		set_seq : function(q){
    			seq = q;
    		},
    		gensym: function(){
    			var result = prefix + seq;
    			seq++;
    			return result;
    		}
    	};
    };
    
    var seqer = serial_marker();
    seqer.set_prefix('Q');
    seqer.set_seq(1000);
    document.writeln(seqer.gensym());
    document.writeln(seqer.gensym());
    
     
       seqer 包含的方法都没有用到this或that. 因此没有办法损害seqer. 虽然sequer对象是可变的,所以它的方法可能被替换掉,但替换后的方法依然不能访问私有成员.seqer 就是一组函数的集合,而且那些函数被授予特权,拥有使用或修改私有状态的能力。如果我们把seqer.gensym 作为一个值传递给第三方函数,那个函数能用它产生位移字符串。但却不能通过它来改变prefix 或 seq 的值.

你可能感兴趣的:(JavaScript,设计模式,编程,prototype,读书)