【读书笔记】读《JavaScript高级程序设计-第2版》 - 函数部分
1. 定义
函数实际上是对象,每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。
对于函数的定义有以下三种类型:
函数声明式定义——如
1 function sum(num1, num2) {return num1 + num2;}
函数表达式定义——如
1 var sum = function(num , num2) {return num1 + num2;}; //注意函数末尾有一个分号,就像声明其他变量一样
Function构造函数——如
1 var sum = Function("num1", "num2", "return num1 + num2"); //不推荐,但是这种写法对于理解“函数是对象,函数名是指针”的概念倒是非常直观的。
由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有区别,也就是说,一个函数可能会有多个名字,如
1 function sum(num1, num2) {return num1 + num2;}
2 var anotherSum = sum; //注意:使用不带圆括号的函数名是访问函数的指针
3 sum = null;
4 alert(anotherSum(10, 10)); //20
2. 没有重载
将函数名作为指针,很容易理解为什么js中没有函数重载的概念。
1 function addNum(num) {return num + 100;}
2 function addNum(num) {return num + 200;}
3 var result = addNum(100); //300
显然后面的函数覆盖了前面的函数。
以下做以等效替换——
1 var addNum = function(num) {return num + 100;}
2 addNum = function(num) {return num + 200;}
3 var result = addNum(100); //300
因此说,在创建第二个函数的时候,实际上覆盖了引用第一个函数的变量addNum。
3. 函数声明与函数表达式
解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。或者理解为第一种定义方式会在代码执行之前被加载到作用域中,而后者则是在代码执行到那一行的时候才会有定义。
4. 作为值的函数
因为js中的函数名本身就是变量,所以函数可以当做值来使用。也就是说,不仅可以向传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。
5. 函数内部属性(函数被调用后所具有的属性)
每个函数在被调用时,其活动对象都会自动取得两个特殊的对象:arguments和this。
arguments是一个类数组对象,包含着传入函数中的所有参数,这个对象还有一个名为callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数。
this引用的是函数据以执行操作的对象,或者说是函数在执行时所处的作用域。(当在网页的全局作用域中调用函数时,this对象引用的是window)
1 window.color = "red";
2 var o = { color:"blue"};
3 function saycolor() {
4 alert(this.color);
5 }
6 saycolor(); //red:在全局作用域中调用
7 o.saycolor = saycolor;
8 o.saycolor(); //blue //此时this引用的是对象o
注意:函数的名字仅仅是一个包含指针的变量而已,因此,即使是在不同的环境中执行,全局的saycolor()函数与o.saycolor()指向的仍然是同一个函数。
6. 函数本身(固有)属性和方法
每一个函数都有两个属性:length和prototype。其中,length属性表示的是函数希望接受的命名参数的个数,如
1 function sum(num1, num2) {return num1 + num2;}
2 alert(sum.length) //2
prototype属性是保存它们所有实例方法的真正所在。换句话说,诸如toString()和valueOf()等方法实际上都保存在prototype名下,只不过是通过各自对象的实例访问罢了。
每个函数都包含两个非继承而来的方法:apply()和call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。先看apply()方法的使用——
1 function sum(num1, num2) {
2 return num1 + num2;
3 }
4 function callSum1(num1, num2) {
5 return sum.apply(this, arguments); //传递arguments对象
6 }
7 function callSum2(num1, num2) {
8 return sum.apply(this, [num1, num2]); //传递数组
9 }
10 alert(callSum1(10, 10)); //20这里的this作用域是window对象
11 alert(callSum2(10, 10)); //20这里的this作用域是window对象
call()和apply()的作用相同,区别在于接受参数的方式不同。如
1 function callSum3(num1, num2) {
2 return sum.call(this, num1, num2); //传递的参数必须逐个列举出来
3 // return sum.call(this, arguments[0], arguments[1]);
4 }
对于是使用apply()还是call(),完全取决于采取哪种给函数传递参数的方式最方便。
对于这两个方法的真正强大之处在于能够扩充函数赖以运行的作用域。如
1 window.color = "red";
2 var o = { color:"blue"};
3 function saycolor() {
4 alert(this.color);
5 }
6 saycolor(); //red
7 saycolor.call(this); //red
8 saycolor.apply(window); //red
9 saycolor.apply(o); //blue
10
11 //o.saycolor = saycolor; //对象和方法具有一定的耦合
12 //o.saycolor(); //blue
因此,使用call()(或者apply())来扩充作用域的最大好处是对象不需要与方法有任何耦合关系。
7. 理解返回值
函数在定义时不必指定是否返回值。如果函数具有返回值,函数会在执行完return语句之后停止并立即退出,因此,位于return语句之后的任何代码都永远不会执行。另外,return语句也可以不带有任何返回值,在这种情况下,函数在停止执行后将返回undefined值,这种做法一般用在需要提前停止函数执行而又不需要返回值的情况下。如
1 function sayHi(name, msg) {
2 return;
3 alert("Hello " + name + "," + msg); //永远不会被调用
4 }
推荐的做法是要么让函数始终都返回一个值,要么永远都不要返回值。否则,如果函数有时候返回值,有时候不返回值,会给调试代码带来不便。
8. 理解参数
JavaScript函数不介意传递进来多少个参数,也不在乎传进来的参数是什么数据类型。如
1 //常规写法
2 function sayHi(name, msg) {
3 alert("Hello " + name + "," + msg); //永远不会被调用
4 }
5 //变通写法
6 function syaHi() {
7 alert("Hello " + arguments[0] + "," + arguments[1]);
8 }
9 sayHi("King", "How are you?"); //两个函数的执行结果是一致的
之所以会这样,原因是JavaScript中的参数在内部是用一个数组来表示的。函数接收到的始终是一个数组,而不关心数组汇总包含哪些参数。如果这个数组不包含任何元素,无所谓;如果包含多个参数,也没有问题。
可以通过arguments.length属性可以获知有多少个参数传递了函数。
当然,还可以arguments对象和命名参数一起使用,如
1 function doAdd(num1, num2) {
2 if (arguments.length == 1) {
3 alert(num1 + 10);
4 } else if (arguments.length == 2) {
5 alert(num1 + num2);
6 }
7 }
没有传递值的命名参数将自动被赋予undefined值,这就跟定义了变量但又没有初始化一样。
9. 匿名函数——递归
一个非常经典的递归阶乘函数:
1 function factorial(num) {
2 if (num <= 1) {
3 return 1;
4 } else {
5 return num * factorial(num - 1);
6 }
7 }
8 var anotherFactorial = factorial;
9 factorial = null;
10 alert(anotherFactorial(4)); //出错,源于内部定义已经引用方法factorial
方法改进如下:
1 function factorial(num) {
2 if (num <= 1) {
3 return 1;
4 } else {
5 return num * arguments.callee(num - 1); //arguments.callee指代的是被调用者
6 }
7 } //方法经改进后alert(anotherFactorial(4));就不会出错了
10. 匿名函数——闭包
闭包指有权访问另一个函数作用域中的变量的函数。(最核心的目的是从函数的外部读到函数的内部变量)
1 function f1(){
2 var n=999;
3 //它本身是一个匿名函数,同时也是一个闭包
4 //这里的变量nAdd是一个全局变量
5 nAdd=function(){
6 n+=1;
7 }
8 function f2(){
9 //可以对上层函数f1的局部变量进行任何操作
10 alert(n);
11 }
12 return f2;
13 }
14 //闭包是一个函数,这个函数可以访问到另一个函数的局部变量
15 var result=f1(); //由于闭包函数的存在,使得f1()函数中的局部变量被保存在内存中,使得当f1()函数被调用后,其内部的变量n依旧存在,并没有在f1调用后被自动清除
16 //n一直保存在内存中的原因剖析:
17 //f1是f2的父函数,而f2被赋给了一个全局变量result,这导致f2始终在内存中,
18 //而f2的存在依赖于f1,因此f1也始终在内存中,不会再调用结束后,被垃圾收集机制回收
19 result(); // 999
20 nAdd(); //因为n一直保存在内存中,nAdd()本身就是一个闭包,nAdd()是对其上层父函数f1中局部变量的操作,所以n的值变为了1000
21 result(); // 1000
思考——
1 var name = "The Window";
2 var object = {
3 name : "My Object",
4 getNameFunc : function(){
5 return function(){
6 return this.name; // The Window
7 };
8 }
9 };
10 alert(object.getNameFunc()()); //The Window ??
this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被当做某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。
为什么会这样呢?原因是在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。每个函数再被调用的时候,其活动对象都会自动取得两个特殊变量:this和arguments。内部函数在搜索者两个变量时,只会搜索到其活动对象为止,因此不可能直接访问到外部函数中的这两个变量。
改进:把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下所示:
1 var name = "The Window";
2 var object = {
3 name : "My Object",
4 getNameFunc : function(){
5 var that = this;
6 return function(){
7 return that.name; // My Object
8 };
9 }
10 };
11 alert(object.getNameFunc()()); // My Object
注意:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能导致内存占用过多,因此建议只在绝对必要时再考虑使用闭包。
11. 匿名函数——模仿块级作用域
1 function output(count) {
2 for(var i = 0; i < count; i++){
3 alert(i);
4 }
5 var i;//JavaScript不会告诉是否多次声明了变量,只会对后续的声明视而不见,但会改变其值
6 }
从i有了定义开始,就可以在函数内部随处访问它。
我们想要在i使用完之后立即销毁掉。可以用匿名函数来模仿块级作用域来实现。
1 (function(){
2 //块级作用域
3 })()
注意:不可以丢掉左边的小括号,即需要将函数封装在一对括号中。否则js将function关键字当做一个函数声明的开始,而函数声明后面不能跟圆括号。对于函数表达式的写法,如
1 var someFunc = function(){};
2 someFunc();
其表达式变量someFunc随后跟了一个小括号,代表函数的调用。因此,同理而言,(function(){//块级作用域})()的左侧小括号代表一个匿名函数表达式,而后边的小括号代表该匿名函数的执行。因此,其内部的变量在执行后随即被销毁。
所以,无论在什么地方,只要临时需要一些变量,就可以使用块级作用域(也称为私有作用域)。对于上面的例子可改造如下:
1 function output(count) {
2 (function(){
3 //能够访问到count变量是因为块级作用域本身是一个闭包
4 for(var I = 0; I < count; i++){
5 alert(i);
6 }
7 )();
8 alert(i); //报错
9 }
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,我们应该尽量减少向全局作用域中添加变量和函数。在一个由很多人开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突。通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。而且,这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
12.关于匿名函数,总结如下:
1>任何函数表达式从技术上说都是匿名函数,因为没有引用它们的确定的方式;
2>递归函数应该始终使用arguments.callee来递归地调用自身,不要使用函数名,因为函数名可能会发生变化;
3>当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量;是源于:
a>在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域;
b>通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止;
4>使用闭包可以在JavaScript中模仿块级作用域,要点如下:
a>创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用;
b>结果就是函数内部的所有变量都会被立即销毁,除非将某些变量赋值给了包含作用域中的变量;
【读书笔记】读《JavaScript高级程序设计-第2版》 - 函数部分的更多相关文章
- 读书笔记(javascript 高级程序设计)
一. 数据类型: 1. undefined: 未声明和未初始化的变量,typeof 操作符返回的结果都是 undefined:(建议未初始化的变量进行显式赋值,这样当 typeof 返回 undefi ...
- 读书笔记:javascript高级程序设计
> 变量.作用域和内存问题js为弱类型的语言 变量的值和数据类型可以在脚本的生命周期内改变.5种基本类型:string, number, undefined, null, boolean,基本数 ...
- 读书时间《JavaScript高级程序设计》三:函数,闭包,作用域
上一次看了第6章,面向对象.这里接着看第7章. 第7章:函数表达式 定义函数有两种方式:函数声明.函数表达式 //函数声明 function functionName(arg0,arg1,arg2){ ...
- [已读]JavaScript高级程序设计(第3版)
从去年开始看,因为太长,总是没有办法一口气把它看完,再加上它与第二版大部分一致,读起来兴致会更缺一点. 与第二版相比,它最大的改变就是增加了很多html5的内容,譬如:Object对象的一些新东西,数 ...
- 读Javascript高级程序设计第三版第六章面向对象设计--创建对象
虽然Object构造函数或者对象字面量都可以用来创建单个对象,但是缺点非常明显:使用同一接口创建很多对象,会产生大量重复代码. 工厂模式 1 function CreatePerson(name,a ...
- [已读]JavaScript高级程序设计(第2版)
经典红皮书~~
- 读javascript高级程序设计00-目录
javascript高级编程读书笔记系列,也是本砖头书.感觉js是一种很好上手的语言,不过本书细细读来发现了很多之前不了解的细节,受益良多.<br/>本笔记是为了方便日后查阅,仅作学习交流 ...
- 读javascript高级程序设计-目录
javascript高级编程读书笔记系列,也是本砖头书.感觉js是一种很好上手的语言,不过本书细细读来发现了很多之前不了解的细节,受益良多.<br/>本笔记是为了方便日后查阅,仅作学习交流 ...
- 读书时间《JavaScript高级程序设计》一:基础篇
第一次看了<JavaScript高级程序设计>第二版,那时见到手上的书,第一感觉真是好厚的一本书啊.现在再次回顾一下,看的是<JavaScript高级程序设计>第三版,并记录一 ...
- JavaScript高级程序设计第三版.CHM【带实例】
从驱动全球商业.贸易及管理领域不计其数的复杂应用程序的角度来看,说 JavaScript 已经成为当今世界上最流行的编程语言一点儿都不为过. JavaScript 是一种非常松散的面向对象语言,也是 ...
随机推荐
- Hibernate之一对多(多对一)
一.双向关联级联保存客户订单 1.搭建环境,项目结构如下 2.代码及配置如下(数据库里订单表不能用order,因为order是数据库关键字)(客户外键cid和订单表外键cid要在配置中写一致) pac ...
- javascript学习随笔(二)原型prototype
JavaScript三类方法: 1.类方法:2.对象方法:3.原型方法;注意三者异同 例: function People(name){ this.name=name; //对象方法 this.Int ...
- C#实现eval 进行四则运算
昨天在园子里看到有园友,写了相同标题的一篇文章.重点讲的是中缀表达式转换为后缀表达式的算法,但是实现的四则运算 有bug.其实我没看之前也不懂什么是 中缀和后缀表达式,之前有用过js eval 内置函 ...
- memcache内存分配机制
memcached的内存分配没有用到c语言中自带的malloc函数,因为这个函数分配内存的时候效率很低,对于这种要求快速响应,对效率要求非常高的缓存软件来说非常不合适. memcached用的是自己的 ...
- Linux下安装php screw
1.下载地址:http://sourceforge.net/projects/php-screw/ 2. tar zxvf php_screw_1.5.tar.gz cd php_screw_1.5 ...
- centos命令
alt + z 打开终端(自定义命令) su 切换到root
- LNMP安装成功的界面
在ubuntu13.10上面安装一个lnmp集成环境. 下面是安装成功的界面. ===========================add nginx and php-fpm on startup ...
- UVa12726 one Friend at a Time (位 广搜)
题目链接:UVa12726 是个PDF,不好复制进来. 大意:有个人要追个妹子,想加妹子QQ,但是不知道谁规定的,玩QQ的人要加好友必须先要有至少k个共同好友.共有N个人玩QQ,编号为1到N,1是男主 ...
- seajs之seajs-debug坑
最近遇到两个关于seajs-debug的坑 一个与preload有关,详情见https://github.com/seajs/seajs-debug/issues/15 一个与map时间戳有关,详情见 ...
- Sublime text 3 中文文件名显示方框怎么解决?
如图,中文文件名打开全是乱码,内容倒是装了converttoutf8没什么太大的问题. -------------------------------------------------------- ...