zoukankan      html  css  js  c++  java
  • 【JavaScript高级程序设计】读书笔记之一 —— 理解函数

    一、定义函数

    定义函数的两种方式:

    (1)函数声明

    function func(arg0, arg1) {
    	// 函数体
    }
    

    (2)函数表达式

    var func = function(arg0, arg1) {
    	// 函数体
    };
    

    它们之间是有很大区别的:

    1)第一个区别:函数声明在{}后可以不需要添加分号,而函数表达式需要

    为什么?

    示例:

    /**
     *   执行报异常:(intermediate value)(intermediate value)(...) is not a function(…)
     *   函数myMethod直接运行了,因为后面有一对括号,而且传入的参数是一个方法。
     *   myMethod返回的42被当做函数名调用了,导致出错了。
     */
    var myMethod = function() {
        console.log('myMethod run'); //执行
        return 42;
    }  // 这里没有分号
    (function() {
        console.log('main run'); //未执行
    })();
    

    而函数声明则不会,“是因为JavaScript将function关键字看做是一个函数声明的开始,而函数声明后面不允许跟圆括号。” —— P185

    2)第二个区别:函数声明提升(function declaration hoisting)

    即在执行代码之前会读取函数声明。

    示例:

    sayHi();	 // 无误,会出现函数声明提升
    function sayHi(){
    	console.log('Hi');
    }
    
    sayHi();	// TypeError: sayHi is not a function #此时函数还不存在
    var sayHi = function(){
    	console.log('Hi');
    }
    

    这里有一个经典的例子:

    /**
     * 表面上看是表示在condition为true时,使用第一个定义,否则使用第二个定义
     * 实际上,这在ECMAScript中属于无效语法,Javascript引擎会尝试修正错误,转换到合理的状态
     * 不要这样做,会出现函数声明,某些浏览器会返回第二个声明,不考虑condition的值
     */
    if(condition) {
     	function func() {
    		console.log('Hi.');
    	}
    } else {
    	function func() {
    		console.log('Hello.');
    	}
    }
    

    下面是推荐的写法,这样就不会有什么意外。

    var func;
    if(condition) {
    	func = function() {
    		console.log('Hi.');
    	}
    } else {
    	func = function() {
    		console.log('Hello.');
    	}
    }
    

    另一个值得一提的是变量名提升:

    var a = 2;
    

    以上代码其实会分为两个过程,一个是 var a; 一个是 a = 2; 其中var a; 是在编译过程中执行的,a = 2 是在执行过程中执行的。
    例:

    console.log(a);	 // undefined
    var a = 2;
    

    其执行效果实际上是这样的:

    var a;
    console.log( a );	// undefined
    a = 2;
    

    在编译阶段,编译器会将函数里所有的声明都提前到函数体内的上部,而真正赋值的操作留在原来的位置上,这也就是上面的代码打出undefined的原因。否则的话应该是报错:Uncaught ReferenceError: a is not defined

    二、递归函数

    下面根据经典的递归阶乘函数的示例分析递归的使用。递归函数的使用可以分为以下几个不同的境界:

    (1)初级版

    function factorial(num) {
    	if(num <= 1) {  //每一个递归都必须有一个终止条件
    		return 1;
     	}
    	return num * factorial(num-1);
    }
    

    分析: 这个递归的调用正常使用没什么问题,但是当我们将另一个变量也指向这个函数,将原来的指向函数的引用变量赋为null会导致错误:

    var anotherFactorial = factorial;
    factorial = null;
    anotherFactorial(10);   //error, factorial已不再是函数
    

    (2)进阶版

    function factorial(num) {
    	if(num <= 1) {
    		return 1;
    	}
    	return num * arguments.callee(num-1);
    }
    

    分析: arguments.callee是一个指向正在执行的函数的指针,比直接使用函数名保险。不过在严格模式下('use strict'),访问这个属性会导致错误。

    (3)高级版(命名函数表达式)

    var factorial = (function f(num) {
    	if(num <= 1) {
    		return 1;
    	}
    	return num * f(num-1);
    });
    

    分析: 一般函数表达式都是创建一个匿名函数,并将其赋值给变量——函数表达式(上述例子是不会进行函数声明提升的)。
    但是此处是创建了一个名为f()的命名函数表达式。即使赋值给了另一个变量,函数的名字 f 仍然有效,所以递归不论是在严格模式还是非严格模式下都照样能正确完成。

    console.log(factorial.name);    // 'f'
    

    **疑惑:在这个函数的外部是不能通过`f`访问这个函数的,为什么?**

    ffactorial 能调用这个函数,说明 ffactorial 是都是一个指向该函数的指针。但是 f 在函数外部是不调用的,说明 f 应该是在函数的内部??但函数作用域不应该是在 {} 内部吗??

    三、闭包

    闭包的定义:有权访问另一个函数的作用域中的变量的函数。也就是说,闭包是内部函数以及其作用域链组成的一个整体。

    闭包主要有两个概念:可以访问外部函数,维持函数作用域。
    第一个概念并没有什么特别,大部分编程语言都有这个特性,内部函数可以访问其外部变量这种事情很常见。所以重点在于第二点。

    创建闭包的常见方式:在一个函数内创建另一个函数。

    示例:

    var globalValue;
    function outter() {
     	var value = 1;
    	function inner() {
    		return value;
    	}
    	globalValue = inner;
    } 
    outter(); 
    globalValue();  // return 1;
    

    先不考虑闭包地看一下这个问题:

    • 首先声明了一个全局变量和一个 outter 函数。
    • 然后调用了 outter 函数,调用函数的过程中全局变量被赋值了一个函数。
    • outter 函数调用结束之后,按照内存处理机制,它内部的所有变量应该都被释放掉了,不过我们把 inner 赋值给了全局变量,所以还可以在外部调用它。
    • 接下来我们调用了全局变量,这时候因为 outter 内部作用域已经被释放了,所以应该找不到 value 的值,返回的应该是 undefined
    • 但事实是,它返回了 1 ,即内部变量。本该已经消失了,只能存在于 out 函数内部的变量,走到了墙外。这就是闭包的强大之处。

    实际的执行流程:

    • 当创建 outter 函数时,会创建一个预先包含全局变量对象的作用域链,保存在内部的 [[Scope]] 属性中,如下图。
    • 当调用 outter 函数时,会创建执行环境,然后通过复制函数的 [[Scope]] 属性中的对象构建执行环境的作用域链,并初始化函数的活动对象(activation object)。
    • outter 函数执行完毕之后,其执行环境的作用域链被销毁,但它的活动对象仍然会留在内存中。
    • 直到对 inner 函数的引用解除后, outter 函数的活动对象才会被销毁 (globalValue = null;)

    在某个构造函数中查看 [[Scope]]属性:
    -[[Scope]]-

    闭包会保存包含函数的活动对象:

    • 闭包与变量:闭包保存的不是某个变量对象,而是包含函数的整个变量对象,并且只能取得包含函数中任何变量的最后一个值。

    这里的例子除了书中的一个经典的例子外,在MDN上有一个更好的、更直观的例子,参见 MDN 在循环中创建闭包:一个常见错误,示例如下

    数组 helpText 中定义了三个有用的提示信息,每一个都关联于对应的文档中的输入域的 ID。通过循环这三项定义,依次为每一个输入域添加了一个 onfocus 事件处理函数,以便显示帮助信息。

    运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个输入域上,显示的都是关于年龄的消息。

    该问题的原因在于赋给 onfocus是闭包(setupHelp)中的匿名函数而不是闭包对象;在闭包(setupHelp)中一共创建了三个匿名函数,但是它们都共享同一个环境(item)。在 onfocus 的回调被执行时,循环早已经完成,且此时 item 变量(由所有三个闭包所共享)已经指向了 helpText 列表中的最后一项。

    解决这个问题的一种方案是使onfocus指向一个新的闭包对象。

    这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境, makeHelpCallback 函数为每一个回调创建一个新的环境。在这些环境中,help 指向 helpText 数组中对应的字符串。

    上面的代码相当于将每次迭代的item.help复制给参数argus(因为函数参数都是按值传递的),这样在匿名函数内部创建并返回的是一个访问的这个argus的闭包。

    document.getElementById(item.id).onfocus = function(argus) {
    	return function() {
    		showHelp(argus);
    	};
    }(item.help);
    
    • 因为闭包会携带包含它的函数的作用域,所以闭包会比其他函数占用更多的内存,所以慎重使用闭包。

    尤其是当在闭包中只使用包含函数的一部分变量,可以释放无用的变量。例如:

    var foo = function(){
    		var elem = $('.demo');
    		return function(elem.length){
    			// 函数体
    		}
    	}
    

    改写为:

    var foo = function(){
    		var elem = $('.demo'),
    		  	len = elem.length;
    		elem = null;	// 解除对该对象的引用
    		return function(len){
    			// 函数体
    		}
    }
    

    四、闭包中的this对象

    this对象是在运行时基于函数的执行环境绑定的:

    • 在全局函数中,this等于window
    • 在某个对象的方法中,this等于这个对象
    • 在匿名函数中,this等于window

    示例1:

    var name = 'The Window';
    var obj = {
    	name: 'My Object',
    	getNameFunc: function(){
    		return this.name;
    	}
    }
    console.log(obj.getNameFunc());   // My Object
    

    示例2:

    var name = 'The Window';
    var obj = {
    	name: 'My Object',
    	getNameFunc: function(){
                var name = "shih";
                return this.name;
    	}
    }
    console.log(obj.getNameFunc());   // My Object
    

    示例3:

    var name = 'The Window';
    var obj = {
    	name: 'My Object',
    	getNameFunc: function(){
    		return function(){	// 匿名函数的执行环境具有全局性
    			return this.name;
    		}
    	}
    }
    console.log(obj.getNameFunc()());	// The Window
    

    还有一个例子:(obj.getNameFunc = obj.getNameFunc)(); // The Window

    this永远指向的是最后调用它的对象,匿名函数的执行环境具有全局性,匿名函数的调用者是window.

    疑惑:匿名函数的this指向为什么是window —— 对于返回的闭包(匿名函数)与函数表达式创建的匿名函数?

    知乎上有一些关于这个问题的回答,百家之言,都不一定正确

    下面的例子是一个测试,其中obj2定义这两种匿名函数,执行结果在注释中,this 对象都是指向 Window

    var name = 'The Window';
    var obj = {
        name: 'My Object',
        getNameFunc0:  function(){
            return this.name;	// "My Object"
        },
        obj2: {
        	// obj2 对象中没有定义 name
        	getNameFunc1:  function(){
    	        var func = function(){
    	        	console.group('getNameFunc2 func Anonymous');
    		        	console.log(this);	// Window
    		        console.groupEnd();
    	        };
    	        func();
    
    	        console.group('getNameFunc');
    		        console.log(this);	// Object
    		    console.groupEnd();
    
    	        return this.name;	// undefined
    	    },
    	    getNameFunc2:  function(){
    	        return function(){
    	        	console.group('getNameFunc2 Anonymous');
    		        	console.log(this);	// Window
    		        console.groupEnd();
    
    	        	return this.name;	// "The Window"
    	        }
    	    }
        }
    };
    
    console.log(obj.getNameFunc0());    // "My Object"
    console.log(obj.obj2.getNameFunc1());    // undefiend
    console.log(obj.obj2.getNameFunc2()());    // "The Window"
    

    五、模仿块级作用域

    (1)JavaScript中没有块级作用域的概念,作用域是基于函数来界定的

    在下面的例子中,在 C++、Java等编程语言中,变量 i 只会在for循环的语句块中有定义,循环结束后就会被销毁。但是在JavaScript中,变量 i 是定义在outputNumbers()的活动对象中的,从它定义的地方开始,在函数内部都可以访问它。

    示例:

    function outputNumbers(count){
    	for(var i=0; i<count; i++){
    		// 代码块
    	}
    	console.log(i);	// i = count
    }
    

    重新声明变量时,JavaScript会忽略后续的声明。但是执行后续声明的变量初始化。

    function outputNumbers(count){
    	for(var i=0; i<count; i++){
    		// 代码块
    	}
    	var i;		//重新声明变量,会被忽略
    	console.log(i);	//i = count
    }
    

    (2)利用即时函数模仿块级作用域——私有作用域

    	function outputNumners(count){
    		(function(){	//闭包
    			for(var i=0; i<count; i++){
    				// 代码块
    			}
    		})();
    		console.log(i);//Error: i未定义
    	}
    

    无论在什么地方,只要临时需要一些变量,就可以使用这种私有作用域。因为没有指向该匿名函数的引用,所以只要函数执行完毕,就可以立即销毁其作用域链。因此可以减少闭包占用的内存问题。

    (3)严格的说,在JavaScript也存在块级作用域

    如下面几种情况:

    1)with
    var obj = {a: 2, b: 3, c: 4};
    with(obj) {	// 均作用于obj上
    	a = 5;
    	b = 5;
    }
    
    2)let/const

    let是ES6新增的定义变量的方法,其定义的变量仅存在于最近的{}之内

    var foo = true;
    if (foo) {
    	let bar = foo * 2;
    	console.log(bar);	// 2
    }
    console.log(bar); // ReferenceError
    

    let一样,唯一不同的是const定义的变量值不能修改。

    var foo = true;
    	if (foo) {
    		var a = 2;
    		const b = 3;	// 仅存在于if的{}内
    		a = 3;
    		b = 4;	// 出错,值不能修改
    	}
    console.log(a);	// 3
    console.log(b);	// ReferenceError
    

    六、私有变量

    严格来说,JavaScript中没有私有成员的概念,所有的对象属性都是公开的,但是有私有变量的概念。任何在函数中定义的变量都可以认为是私有变量。
    私有变量包括:函数的参数、局部变量、在函数内部定义的其他函数。

    因为函数外部不能访问私有变量,而闭包能够通过作用域链可以访问这些变量。所以可以创建用于访问私有变量的公有方法 —— 特权方法(privileged method)

    有几种创建这种特权方法的方式:

    (1)构造函数模式(Constructor Pattern)

    function MyObject(){
    
    	// 私有变量和私有函数
    	var privateVariable = 10;
    		function privateFunction(){
    		return false;
    	}
    
    	// 公有方法,可以被实例所调用
    	this.publicMethod = function(){
    		++privateVariable;
    		return privateFunction();
    	};
    }
    

    这种模式的缺点是,针对每个实例都会创建一组相同的方法。

    (2)原型模式(Prototype Pattern)

    //创建私有作用域,并在其中封装一个构造函数和相应的方法
    (function(){
    
    	//私有变量和私有函数
    	var privateVariable = 10;
    
    	function privateFunction(){
    		return privateVariable;
    	}
    
    	//构造函数,使用的是函数表达式,因为函数声明只能创建局部函数
    	MyMethod = function(){
    	};
    
    	//公有方法
    	MyMethod.prototype.publicMethod = function(){
    		++privateVariable;
    		return privateFunction();
    	};
    })();
    

    公有方法是在原型上定义的。这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式,这是因为函数声明只能创建局部函数,这不是我们想要的。同样,在声明MyObject时也没有使用var关键字,因为直接初始化一个未经声明的变量,总会创建一个全局变量。因此MyObject就成了一个全局变量,能够在私有作用域之外被访问到。但值得注意的是,在严格模式('use strict')下,给未经声明的变量赋值会导致错误。

    这个公有方法作为一个闭包,总是保存着对作用域的引用。与在构造函数中定义公有方法的区别是:因为公有方法是在原型上定义的,所有实例都使用同一个函数,私有变量和函数是由实例所共享的。但上面的代码有个缺陷,当创建多个实例的时候,由于变量也共享,所以在一个实例上调用publicMethod会影响其他实例。以这种方式创建的静态私有变量会因为使用原型而增加代码的复用,但每个实例都没有自己的私有变量。到底是使用实例自己的变量,还是上面这种静态私有变量,需要视需求而定。

    正是由于上述原因,我们很少单独使用原型模式,通常都是将构造函数模式结合原型模式一起使用

    (3)模块模式(Module Pattern)

    以上的模式都是给自定义类型创建私有变量和特权方法的。而这里所说的模块模式则是为单例创建私有变量和特权方法,增强单例对象。

    1)单例模式(Singleton Pattern)

    单例模式是指只有一个实例的对象。
    JavaScript推荐使用对象字面量的方式创建单例对象:

    var singleton = {
    	name: 'value',
    	method: function(){
    		// 代码块
    	}
    };
    
    2)模块模式通过为单例增加私有变量和公有方法使其得到增强
    var singleton = function(){
    
    	// 私有变量和私有函数
    	var privateVariable = 10;
    
    	function privateFunction(){
    		return false;
    	}
    
    	// 公有方法:返回对象字面量,是这个单例的公共接口。
    	return{
    		publicProperty: true,
    		publicMethod: function(){
    			++privateVariable;
    			return privateFunction();
    		};
    	}
    }
    

    “如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,就可以使用模块模式。” —— P190

    (4)增强的模块模式

    var singleton = function(){
    
    	// 私有变量和私有函数
    	var privateVariable = 10;
    
    	function privateFunction(){
    		return false;
    	}
    
    	// 创建一个特定的对象实例
    	var object = new CustomType();
    
    	// 添加属性和方法
    	object.publicProperty = true;
    	object.publicMethod = function(){
    		++privateVariable;
    		return privateFunction();
    	};
    	return object;
    }
    

    创建一个特定类型的实例,即适用于那些单例必须是某种特定类型的实例,同时还需要对它添加一些属性和方法加以增强。

    七、即时函数与闭包的异同

    闭包:

    var foo = function(){
    	// 声明一些局部变量
    	return function(){    // 闭包
    		// 可以引用这些局部变量
     	}
    }
    foo()();	// 可以对foo函数内的局部变量进行操作,具体方法在闭包函数的定义中
    

    即时函数:

    (function(){
    	// 执行代码
    })();
    

    相同点:它们都是函数的一种特殊形态,并且可以共存。
    不同点:即时函数是定义一个函数,并立即执行。它只能被使用一次,相当于“阅后即焚”。它是为了形成块级作用域,来弥补js函数级作用域的局限,主要是为了模块化,很多库都这么来解决耦合,而且考虑到没有加分号 ; 会导致错误的原因,很多库都会在开始处加上 ;
    比如jquery.media.js :

    ; (function($) {
    	"use strict";	
    
    	var mode = document.documentMode || 0;
    	var msie = /MSIE/.test(navigator.userAgent);
    	var lameIE = msie && (/MSIE (6|7|8).0/.test(navigator.userAgent) || mode < 9);
    	// ...
    	
    })(jQuery);
    

    闭包是指一个函数与它捕获的外部变量的合体。用来保存局部变量,形成私有属性与方法,比如module模式。

    参考

    1. 闭包与变量的经典问题
    2. 在循环中创建闭包:一个常见错误
    3. 聊一下JS中的作用域scope和闭包closure
  • 相关阅读:
    测试框架Mockito使用笔记
    Apache-Shiro+Zookeeper系统集群安全解决方案之缓存管理
    C#学习笔记-数据的传递以及ToolStripProgressBar
    如果我们不曾相遇
    C#学习笔记-数据的传递(公共变量)以及Dictionary
    C#学习笔记-icon托盘图标的简单知识
    C#学习笔记-Windows窗体基本功能(Login登录界面)
    CellSet 遍历
    DevExpress PivotGrid 使用记录
    Funsioncharts 线图 破解
  • 原文地址:https://www.cnblogs.com/shih/p/6826750.html
Copyright © 2011-2022 走看看