【JavaScript高级程序设计】读书笔记之一 —— 理解函数
目录
- 一、定义函数
- 二、递归函数
- 三、闭包
- 四、闭包中的this对象
- 五、模仿块级作用域
- 六、私有变量
- 七、即时函数与闭包的异同
- 参考
一、定义函数
定义函数的两种方式:
(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`访问这个函数的,为什么?** `f` 和 `factorial` 能调用这个函数,说明 `f` 与 `factorial` 是都是一个指向该函数的指针。但是 `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]]
属性:
闭包会保存包含函数的活动对象:
- 闭包与变量:闭包保存的不是某个变量对象,而是包含函数的整个变量对象,并且只能取得包含函数中任何变量的最后一个值。
这里的例子除了书中的一个经典的例子外,在MDN上有一个更好的、更直观的例子,参见 MDN 在循环中创建闭包:一个常见错误,示例如下
该浏览器不支持iframe
数组 helpText
中定义了三个有用的提示信息,每一个都关联于对应的文档中的输入域的 ID
。通过循环这三项定义,依次为每一个输入域添加了一个 onfocus
事件处理函数,以便显示帮助信息。
运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个输入域上,显示的都是关于年龄的消息。
该问题的原因在于赋给 onfocus
是闭包(setupHelp)中的匿名函数而不是闭包对象;在闭包(setupHelp)中一共创建了三个匿名函数,但是它们都共享同一个环境(item)。在 onfocus
的回调被执行时,循环早已经完成,且此时item
变量(由所有三个闭包所共享)已经指向了 helpText
列表中的最后一项。
解决这个问题的一种方案是使onfocus
指向一个新的闭包对象。
该浏览器不支持iframe
这段代码可以如我们所期望的那样工作。所有的回调不再共享同一个环境, 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 对象中没有定义 namegetNameFunc1: function(){var func = function(){console.group('getNameFunc2 func Anonymous');console.log(this); // Windowconsole.groupEnd();};func();console.group('getNameFunc');console.log(this); // Objectconsole.groupEnd();return this.name; // undefined},getNameFunc2: function(){return function(){console.group('getNameFunc2 Anonymous');console.log(this); // Windowconsole.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模式。
参考
- 闭包与变量的经典问题
- 在循环中创建闭包:一个常见错误
- 聊一下JS中的作用域scope和闭包closure
转载于:https://www.cnblogs.com/shih/p/6826750.html
【JavaScript高级程序设计】读书笔记之一 —— 理解函数相关推荐
- JavaScript高级程序设计-读书笔记(6)
第20章 JSON JSON是一个轻量级的数据格式,可以简化表示复杂数据结构的工作量 JSON的语法可以表示一下三种类型的值 l 简单值:使用与JavaScript相同的语法,可以在JS ...
- JavaScript高级程序设计读书笔记(一)
第一章 总结: JavaScript是一种专为与网页交互设计的脚本语言. 由三个不同的部分组成: ECMAScript, 由ECMA-262定义,提供核心语言功能: DOM(文件对象模型),提供访问和 ...
- javascript高级程序设计读书笔记2
<!DOCTYPE HTML>//这个网页的文档类型,这个是html5的写法 Bootstrap使用的某些HTML元素和CSS属性需要文档类型为HTML5 doctype.因此这一文档类型 ...
- Javascript高级程序设计--读书笔记--第八章BOM
BOM(browser object model 浏览器对象模型) BOM的核心对象是window,它表示浏览器的一个实例.在浏览器钟,window对象有双重角色,它既是通过javascript 访问 ...
- javascript高级程序设计读书笔记----引用类型
Array类型. ECMAScript数组的每一项可以保存任何类型的数据. 数组大小是可以动态调整的. 创建数组第一种基本方式方式: 使用Array构造函数 var colors = new Arra ...
- JavaScript高级程序设计读书笔记(第8章BOM之location对象查询字符串参数)
为什么80%的码农都做不了架构师?>>> location对象查询字符串参数 //location.search返回从问号到URL末尾的所有内容function getQuerySt ...
- JavaScript高级程序设计读书笔记(第6章面向对象的程序设计之创建对象)
2019独角兽企业重金招聘Python工程师标准>>> 面向对象语言都有"类"的概念,而通过类可以创建任意多个具有相同属性和方法的对象. JS中没有"类 ...
- javascript高级程序设计读书笔记1
浮点数虽然精度很高,但在实际计算中的精度可能不如整数.比如0.1+0.2并不等于0.3,而是0.3000000000000004,所以把浮点数的计算结果当做判断条件会产生失误,这是基于IEEE754数 ...
- JavaScript高级程序设计读书笔记(第5章引用类型之Array类型)
为什么80%的码农都做不了架构师?>>> 1.数组长度 //lenth会自动更新 var arr=["black","red","g ...
- 《javascript高级程序设计》笔记:内存与执行环境
上一篇:<javascript高级程序设计>笔记:继承 近几篇博客都会围绕着图中的知识点展开 由于javascript是一门具有自动垃圾收集机制的编程语言,开发者不必担心内存的分配和回收的 ...
最新文章
- ASP.NET2.0 文本编辑器FCKeditor的冰冷之心 【月儿原创】
- python下selenium测试报告整合
- 1.2 函数间隔和几何间隔理解2
- 使用dom breakpoint找到修改属性的javascript代码
- 取得数组下标_《零基础C++入门教程》——(8)搞定二维数组与循环嵌套
- 前景检测算法 (GMM)
- Jmeter启动失败
- 5分钟商学院-个人篇-高效能人士的思维习惯
- 博观而约取,厚积而薄发
- 人体姿态估计-CPN(三)
- 做什么样的软件才能赚钱?
- linux系统缺少perl组建中的Data::Dumper模块
- Web前端学习笔记(四)--- CSS卡贴悬停展开效果
- 基于SRGAN的图像超分辨率实例
- 阿里云服务器与腾讯云服务器的故事
- 基本回路系统和基本割集系统
- 64位处理器 计算机,32位与64位的处理器有什么区别?
- 检测PE文件的有效性
- python的占位符%d %i %.nf %s
- Java代理模式分析
热门文章
- 使用JSP处理用户注册和登陆
- 小众编程语言同样值得你关注
- 搭建Qt 5.3.1 for Windows Phone 8开发环境
- 使用命令行编译打包运行自己的MapReduce程序 Hadoop2.4.1
- mac ox 10.9 安装eclipse cpp launch failed binary not found
- C++虚函数的实现方式
- main 函数的标准原型
- python六十: __doc__属性
- xmlWriter以UTF-8格式写xml问题
- Informatica PowerCenter使用介绍-转载