《JavaScript高级程序设计》阅读笔记

204 阅读25分钟

第1章 JavaScript简介

一个完整的JavaScript实现由三部分组成:ECMAScript,文档对象模型(DOM),浏览器对象模型(BOM)

ECMAScript: 这一部分可以说是JavaScript的核心,它主要是规定了JavaScript这个语言的一些标准,是JavaScript实现的基础。

文档对象模型(DOM) :它提供一种由XML发展而来,但经过扩展用于HTML的应用程序编程接口,它规定如何映射基于XML的文档结构(以便简化对文档中任意部分的访问和操作),以及规定操作HTML的对象和方法。

浏览器对象模型(BOM) :它提供访问和操作浏览器与框架的方法和接口,开发人员可以通过它去控制页面显示意外的部分。

第2章 在HTML中使用JavaScript

<script>元素

<script>元素的使用方式: 嵌入代码,引入外部文件

嵌入代码:只需要为<script>指定type属性,它内部的js代码将从上至下依次解释,使用这种方式引入JavaScript代码,要注意不要在代码内部出现字符,否则将无法识别,如果非要用,就用转义字符/对这个字符进行转义

引入外部文件:用这种方法还可以引入外部域的js文件,但前提是要确保安全

注意:

  • 如果在引入外部文件的<script>标签对内又包含了嵌入的代码,那么嵌入的代码将被忽略
  • 无论使用这两种方式的哪一种,只要不存在defer或者async属性,浏览器都会按照<script>元素在页面中出现的先后顺序对他们依次进行解析
  • 推荐使用外部文件来包含JavaScript代码,因为它有以下优点:可维护性、可缓存、适应未来

<script>元素几个重要属性:defer,async,type

defer属性(延迟脚本): 如果把<script>元素放在<head>内部,那么页面会等到将页面中的JavaScript代码下载,解析,执行完之后才会去呈现页面的内容,这可能会导致页面有一定的白屏时间,影响用户体验,如果在<script>标签内部用上defer属性就表示立即下载js脚本,但不妨碍页面中的其他操作,比如下载其他资源或等待加载其他脚本,js脚本会延迟到文档完全被解析和显示之后在执行注意:这个属性只能在引入外部文件时有效,而且会按<script>标签的顺序依次先后执行js文件(虽然书中说在现实中可能不会按顺序)。有些浏览器会忽略这个属性,像平常一样处理脚本,所以,把延迟脚本放在页面底部仍然是最佳选择。

async属性(异步脚本) :其他方面均与defer属性相同。唯一的不同不会按照<script>标签的先后顺序去依次执行js文件,所以用这个属性的话,要确保js文件没有相互依赖的关系,因为不知道哪个js文件会先被执行。另外建议不要再加载期间修改DOM。

type属性: 这个属性表示编写代码使用的脚本语言的内容类型(也称为MIME类型),不指定这个属性的话,默认是该属性的值是text/javascript。

<script>元素的位置:<head>内,<body>尾部

<head>:要等<head>内的js文件下载,解析,执行完之后才开始呈现页面的内容(页面遇到body标签才开始呈现内容)

<body>尾部:将js脚本放在<body>元素中页面内容的后面,会在页面加载完之后再去执行js文件

文档模式

文档模式:通过doctype可以切换页面的文档方式,分为混杂模式(怪异模式)和标准模式,如果不设置doctype默认开启混杂模式,不同浏览器在这种模式下的行为差异非常大,所以这是不被推荐的做法。而在标准模式下,浏览器按 W3C 标准解析执行代码。

<noscript>元素

为了让页面在一些比较老的浏览器上平稳退化,这些浏览器可能不支持<script>元素,那么就可以用<noscript>元素显示替代的内容。<noscript>标签的内容只在浏览器不支持脚本,或者支持脚本但脚本被禁用这两种情况下才会显示出来。

第3章 基本概念

语法

标识符:标识符的第一个字母必须是字母、下划线、美元符(注意:不能是数字),其他的字符可以是字符、下划线、美元符、数字。

关键字和保留字

不能把关键字和保留字设置为标识符。

变量

ECMA中的变量是松散类型的,就是说可以用来保存任何类型的变量,每个变量仅仅是一个用于保存值得占位符而已

数据类型

数据类型:ECMAScript中有五种简单数据类型(基本数据类型):Null、Undefined、Number、String、Boolean,以及一种复杂数据类型:Object。

typeof操作符:用于检测变量的数据类型。注意:

  • 它会把null判定为"object",因为null被认为是一个空的对象引用
  • 某些早先的浏览器对正则表达式调用typeof操作符会返回"function",而其他浏览器会返回"object"
  • 对未初始化的变量执行typeof操作符返回"undefined",而且对未声明的变量执行typeof操作符也会返回"undefined".

Boolean类型: 这种类型有两个字面值:true,false,要注意这两个字面值和数字值不是一回事,true不一定等于1,false也不一定等于0

Boolean()转型函数规则:

数据类型转换为true的值转换为false的值
Booleantruefalse
String任何字符串空字符串("")
Number任何非0数字值(包括无穷大)0和NaN
Object任何对象(包括{})null

Number类型: 由于保存浮点数值需要的空间是整数值的两倍,所以ECMAScript会不失时机的把浮点数转换为整数值,比如10.0会被解析为10。另外要注意使用浮点数值计算会产生舍入误差的问题。

数值转换(具体规则用的时候再翻书吧):

  • 有三个函数可以用,Number()可以用于任何数据类型转换为数值,parseInt()专门把字符串转换为整数,parseFloat()专门把字符串转换为浮点数。
  • 因为Number()函数在转换字符串的时候比较复杂且不合理,所以处理整数时推荐使用使用parseInt()函数
  • 使用parseInt()函数的时候最好是明确指定基数,就是指定函数的第二个参数,明确它是二进制,八进制....
  • parseFloat()函数只能解析出十进制值,所以它没有第二个参数制定基数

NaN:用来表示本来要返回数值的操作数未返回数值的情况。

isNaN()函数转换规则:它接受的参数可以是任何类型的值,他会先尝试把这个值转换为数值,任何不能被转换为数值的值都会导致这个函数返回true。

String类型:

  • 字符串可以由单引号(')或者双引号(")表示,不过最好写代码的时候统一一种风格。
  • 数值,布尔值,对象和字符串值都有toString()方法,但注意undefined和null没有这个方法
  • 如果不知道要转换的值是不是有可能为null或者undefined,可以使用转型函数String()

Object类型: Object类型是所有它的实例的基础,也就是说Object类型所具有的的任何属性和方法也同样存在于更具体的对象中(不过ECMA-262不负责定义宿主对象,因此宿主对象可能会也可能不会继承Object)。

Object几个重要的属性与方法:

  • constructor: 保存着用于创建当前对象的函数
  • hasOwnProperty(propertyName): 用于检查给定的属性在当前对象实例中(而不是说在实例的原型中)是否存在,作为参数的属性名必须以字符串形式指定,比如hasOwnProperty("name")
  • isPrototypeOf(object): 用于检查传入的对象是否是传入对象的原型
  • toString(): 返回对象的字符串表示
  • valueOf(): 返回对象的字符串、数值或者布尔值表示,通常情况和toString()返回值相同

操作符

加号(+)操作符:如果有一个操作数是字符串要注意:

  • 如果两个操作数是字符串,将第二个操作数与第一个操作数拼接起来
  • 如果只有一个操作数是字符串,会把另外一个非字符串的操作数转换为字符串,然后将两个字符串拼接起来

相等操作符: 相等(==)和不相等(!=)符号在比较前会先进行数据转换,而且需要注意的一点是对null和undefined使用相等符号,会返回true,这是因为undefined是派生自null值的,但是用全等(===)符号,就会返回false了。

语句

label语句: 可以在代码前面添加标签,然后在循环中配合break和continue语句引用。

break语句与continue语句: break语句会直接退出这个循环,去执行循环之外的语句,而continue则是结束这一次的循环,开始下一次的循环。

函数

注意:

  • 位于return之后的任何代码都永远不会执行
  • 由于不存在函数签名的特性,ECMAScript函数不能重载,如果在ECMAScript中定义了两个名字相同的函数,则改名字只属于后面定义的函数

函数参数:

  • ECMAScript函数不介意传递进来多少个参数,也不介意传进来的参数是什么数据类型。定义函数只接受两个参数,但实际上可以传三个参数进来这样的。
  • 这样的原因是ECMAScript的参数在内部是用一个数组来表示的,在函数体内可以通过arguments对象来访问这个参数数组,arguments对象只是与数组类似
  • arguments对象可以与命名参数一起使用,而且arguments的值永远与对应命名参数的值保持同步。这并不说明读取这两个值会访问相同的内存空间,它们的内存空间相互独立
  • 如果只传入一个参数,那么arguments[1]设置的值不会反应到命名参数中,这是因为arguments对象的长度是由传入的参数个数决定的,不是由定义函数时的命名参数的个数决定的。
function ceshi(num1,num2){     
    console.log(arguments[1]); // undefined   
    console.log(num2); // undefined  
    arguments[1] = 5;   
    console.log(arguments[1]); // 5     
    console.log(num2); //undefined  此时命名参数num2没有跟着变化 
} 
ceshi(4);

第4章 变量、作用域和内存问题

基本类型和引用类型的值

ECMAScript的变量可以包含两种不同类型的值: 一种是基本类型值,一种是引用类型值。基础类型按值访问(就是说可以访问到实际内存中的值),引用类型按引用访问(访问的是对象的引用而不是实际的对象)

复制变量值: 基本类型复制只是把值复制过去了,两个变量之间相互独立,互不影响;引用类型复制的其实是指针,指向同一个对象,所以改变其中一个变量,就会影响另一个变量。

传递参数: 和复制变量值本质是一样的

检测类型:要检测一个引用类型的值,不是想知道这个值是不是对象,而是想知道这个这个值是哪种类型的对象。所以检测对象的类型要用instanceof操作符而不是typeof操作符

执行环境及作用域

变量对象:每一个执行环境都有自己的变量对象,这个对象包含环境中定义的所有变量和函数。

作用域链:

  • 当代码在环境中执行的时候,就会创建变量对象的作用域链。全局环境的变量对象始终存在。而像某个函数这样局部环境的变量对象,只在函数执行的过程中存在。比如说在创建函数的时候,会先创建一个包含全局变量对象的作用域链(没有被使用),等到调用这个函数 的时候,会为这个函数创建一个执行环境,然后创建这个环境的变量对象并推入前面创建的作用域链的前端。
  • 它的用途是保证对执行环境有权访问的所有变量和函数有序访问。
  • 作用域的最前端,永远放的是当前执行环境的变量对象,然后再往后就是父环境的变量对象,最尾端永远是全局执行环境的变量的变量对象
  • 当去找一个变量的时候,总是会从作用域链的最前端依次往后查找,找到了一个就会马上停止,不会再往后查找了。
  • 内部环境可以通过作用域链访问外部环境但外部环境不能访问内部环境的任何变量和函数。
  • 函数参数也被当做变量来对待,因此访问函数参数的规则和访问变量一样。

延长作用域链: 一种是catch,一种是with,使用catch延长作用域链,会创建一个新的变量对象,catch所捕获的错误对象会添加到这个新的变量对象中,添加到最前面?(在IE8以前这个额错误对象会被添加到全局环境的变量对象中,而不是catch语句的变量对象中)

垃圾收集

垃圾收集机制:JavaScript具有自动垃圾收集机制,它的原理就是按照一定的时间间隔去检查没有被使用的变量,释放它们的内存空间。常见的两种垃圾收集方式是标记清除引用计数

标记清除: 标记清除是比较主流的做法,它的原理是当变量进入环境就给变量加上“进入环境”标记,离开时加上“离开环境”标记。这样所有的变量都会被加上标记,除去那些被标上了“进入环境”变量和被这些变量引用的变量,剩下的那些全部删掉。

引用计数: 引用计数的原理是跟踪记录每个值得引用次数,如果有变量a被赋了一个引用类型值object1,这个引用类型值object1得引用次数就加1,如果前面说的变量a又被赋了另外一个引用类型值object2,那这个object1的引用次数就-1,object2的引用次数+1。垃圾收集器下次就删掉引用计数为0的引用类型值

引用计数的问题: Netscape Navigator 早期使用引用计数策略,但是有循环引用的问题,所以后来改成了用标记清除策略。还有一个问题就是,IE中BOM和DOM中的对象并不是原生JavaScript对象,他们是使用C++以COM对象的形式实现的。COM使用的仍然是引用计数,所以只要在IE中涉及COM对象,就会存在循环引用的问题。

  1. 循环引用
object1.a = object2; 
object2.b = object1;// 像这样的互相引用会导致object1和object2的引用次数永远不等于0,所以他们一直不会被清除

 2 .COM:

var element = document.getElementById('ha'); 
object3.element = element; 
element.someobject = object3;
//这样即使将DOM从页面中移除,它也永远不会被回收。 
// 解决办法是:不用他们的时候就手动断开原生JavaScript对象与DOM元素的连接,即设置为null element.someobject = null; object3.element = null; 
// 不过后来IE9把BOM和DOM对象都转换成了真正的JavaScript对象
// 不过像这种解除引用的做法可能让页面获得更好的性能:一旦数据不再有用,就将其值设为null来释放其引用

第5章 引用类型

Object类型

创建Object类型的实例方式:用new操作符后跟Object构造函数、对象字面量

访问Object实例属性: 用点表示、用方括号表示

//用new操作符后跟Object构造函数创建Object实例 
var person1 = new Object(); 
person.name = "lizhe"; 
person.age = 23;  

//对象字面量方法创建Object实例 
var person2 = { 	
    name : "someone", 
    //其实name属性外面也可以加上引号
    "my hometown" :"hunan", //属性名也可以使用字符串,这样属性名中可以包含非字母非数字 	
    age : 23 // 最后一个属性后面不要加逗号,在IE7之前或者Opera浏览器会报错 
} 
var name = person1.name; // 通常情况推荐使用点表示法访问Object实例属性  var propertyname = "name";
var age = person1[propertyname]; 
var propertyname2 = "home town";
var hometown = person2[propertyname2];//方括号访问Object实例属性的主要优点:可以通过变量来访问属性  

Array类型

检验数组

一个经典问题:确认某个对象是不是数组

//使用instanceof操作符检测数组存在一个问题:如果网页中包含多个框架,那它就有多个全局执行环境,也有不同版本的Array构造函数。如果从框架1向框架2中传入一个数组,那么这个数组与框架2中原生创建的的数组分别具有各自不同的构造函数 
if (value instanceof Array){ 	
//... 
}  
// ECMAScript5中新增的这种isArray()方法解决了以上问题,它不会去管数组是在哪个全局执行环境中创建的 
if (Array.isArray(value)){ 
//... 
}

转换方法

转换方法:toLocaleString()、toString()、valueOf()、join()

屏幕快照 2019-08-21 下午12.08.51.png

屏幕快照 2019-08-21 下午12.07.01.png

栈方法

  • push()方法:可以接收任意数量的参数,把他们逐个添加到数组末尾,并返回修改后数组的长度
  • pop()方法接收参数,从数组的末尾移除最后一项,然后返回移除的项

队列方法

  • shift()方法接收参数, 移除数组中的第一项,然后返回移除的项
  • unshift()方法:接收任意数量的参数,把他们逐个添加到数组前端,并返回新数组的长度

重排序方法

  • reverse()方法:反转数组项的顺序
  • sort()方法:这个方法会调用每个数组项的toString()方法,然后比较得到的字符串.所以会有以下问题:
var values = [0, 1, 5, 10, 15]; 
values.sort(); 
console.log(values); // [ 0, 1, 10, 15, 5 ] 
// 这是因为就算数组中的每一项都是数值,但是sort()方法比较的也是转换后的字符串,而进行字符串比较时,"10"在"5"的前面
// 要解决这个问题可以让sort()方法接收一个比较函数作为参数  
function compare(value1, value2) { 	
    if(value1 < value2){ 		
        return -1; 	
    } else if (value1 > value2) { 
        return 1;
    } else { 		
        return 0; 	
    } 
} 
console.log(values.sort(compare));// [ 0, 1, 5, 10, 15 ]

操作方法

  • concat()方法:可以接收任意多个参数或者不传参数,它创建一个新的数组,然后把传入的参数添加到新数组的末尾返回新构建的这个数组(不传参数的时候它就是复制当前数组并返回副本),和push()方法的不同就是它每次返回的是新的数组,操作的也是新的数组,不会对原始数组造成影响
  • slice()方法: 接收一个或者两个参数,即要返回项的起始和结束位置,如果传的是两个参数,那么该方法返回起始和结束位置之间的项,但不包括结束位置的项。这种方法也不会影响原始数组。
  • splice()方法: 接收两个或者大于等于三个参数,接收两个参数时,用法和slice()方法差不多,但他会影响原始数组,而且返回值总是一个包含从原始数组中删除的项的数组
  •  接收大于等于三个参数:起始位置、要删除的项数(可以是0)、要插入的项(可以任意多个)。会影响原始数组,而且返回值也是一个包含从原始数组中删除的项的数组。(没删除就返回空数组

位置方法

  • indexOf()方法: 接收一个两个参数:要查找的项和表示查找起点位置的索引(可选),从数组的开头向后查找,比较时使用的是全等操作符(===), 返回位置索引,没找到返回-1
  • lastIndexOf()方法: 接收一个两个参数:要查找的项和表示查找起点位置的索引(可选),从数组的末尾向前查找,比较时使用的是全等操作符(===), 返回位置索引,没找到返回-1

迭代方法

总共有五种迭代方法,方法都接收一个或者两个参数,第一个参数是一个在每一项上运行的函数,第二个参数是运行该函数的作用域对象(可选)。前面说的这个函数接收三个参数:数组项的值、该项在数组中的位置、数组对象本身。然后对数组中的每一项运行给定函数,不过这些方法的返回值不同。他们都不会影响原始数组。

  • every()方法:如果函数每一项都返回true,那么方法返回true
  • filter()方法:方法返回函数中返回true的每一项所组成的数组
  • forEach()方法:只是对每一项运行给定函数,没有返回值
  • map()方法:返回每次函数结果组成的数组
  • some()方法:如果函数任意一项返回true,那么方法返回true
var numbers = [1, 2, 3, 4];  
var everyResult = numbers.every(function(item, index, array) { 
    return (item > 1); 
}); 
console.log(everyResult,numbers);// false [ 1, 2, 3, 4]  
var filterResult = numbers.filter(function(item, index, array) { 
    return (item > 1); 
});
console.log(filterResult,numbers);// [ 2, 3, 4] [ 1, 2, 3, 4] 
var forEachResult = numbers.forEach(function(item, index, array){ 
    return item++; 
}); 
console.log(forEachResult,numbers);// undefined [ 1, 2, 3, 4 ]  var mapResult = numbers.map(function(item, index, array) {   
    return item--; 
}); 
console.log(mapResult,numbers);// [ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ]  var someResult = numbers.some(function(item, index, array) {  
    return (item > 1);
}); 
console.log(someResult,numbers);// true [ 1, 2, 3, 4]

归并方法

reduce()方法:目的是迭代数组所有的项,构建一个最终返回的。接收一个或者两个参数,第一个参数是一个在每一项上调用的函数,第二个参数是作为归并基础的初始值可选)。前面说到的这个函数,接收四个参数:前一个值、当前值、项的索引和数组对象

reduceRight()方法:和reduce()方法的唯一不同就是,它是从数组的最后一项开始向前遍历到第一项

var values = [1,2,3,4,5]; var sum = values.reduce(function(prev, cur, index, array){ 	return prev + cur;  // 每一次迭代,返回的值都会作为下次迭代的第一个参数prev的值 }); console.log(sum)// 15

Date类型

方法太多了...

RegExp类型

详见另一篇wiki:正则表达式

Function类型

重要概念:函数实际上是对象,每个函数都是Function类型实例,而且都与其他引用类型一样具有属性和方法。因为函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定,所以一个函数可能会有多个名字

函数声明和函数表达式的区别:解析器在加载数据时,会先读取函数声明,使其在执行任何代码之前可以访问。而函数表达式,必须等到解析器执行到他所在的代码行,才会被解释执行。

函数的内部属性:

  • arguments对象有个叫callee的属性指向拥有这个aguments对象的函数。用这个属性可以消除递归阶乘函数的执行与函数名字之间的耦合。严格模式下不能访问这个属性
function factorial(num){ 	
    if (num<=1) { 		
        return 1; 	
    } else { 		
        return num*arguments.callee(num-1);  	
    } 
} //这样不管函数名字变成什么,都能正常递归 
var trueFactorial = factorial; 
console.log(trueFactorial(5));// 120
  • 函数对象有一个叫caller的属性。它指向调用当前函数的那个函数,就是说如果outer函数调用inner函数,那么inner函数的caller属性就指向outer函数。严格模式下不能访问这个属性
  • 函数内部的this:指向的是函数据以执行的环境

函数属性与方法

  • apply() 方法:接收两个参数,一个是运行函数作用域,另一个是参数数组(可以是Araay实例,也可以是arguments对象)
  • call() 方法:第一个参数也是运行函数作用域,但是传递给函数的参数必须逐个列举出来
  • bind() 方法:与前两者的区别是它会创建一个函数的实例,帮它准备好this,参数。但是不会立即执行,可以要用到再去执行

单体内置对象

eval()方法

概念:接收一个参数,这个参数是要执行的ECMAScript代码字符串。当解析到eval方法时,会将eval中传入的参数当做实际的代码语句解析,然后将其执行结果插入到原来位置。如果传入的参数不是字符串,它会原封不动将其返回。

eval(
    "function sayHi() {
        alert ('hi')
    }"
);  
sayHi();

eval的缺点和误解:你不知道的eval

什么时候用eval?

1.eval()可以让写在函数里的代码运行在全局作用域中,根据这个特性:通过eval()执行的代码包含在该次调用的执行环境中,因此被执行的代码具有与该执行环境相同的作用域链。

2.用于代码压缩:webpack构建中有一种eval模式,每个 module 会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL.它的编译和重新编译速度都是最快的。猜测原因可能是因为eval是原生方法 ,原生方法是用C/C++这种变异性语言写出来的,所以用原生方法编译更快?[webpack] devtool配置对比

3.用于早期的JSON解析器。但在旧版本的浏览器中,使用eval方法对JSON数据结构求值存在风险。

4.在canary项目中的使用场景:data-lab中一般存放的是一些json字符串。此处想要把它们解析成json对象赋值给lab变量。

![]( "李哲 > 重读《JavaScript高级程序设计》 > 屏幕快照 2019-09-29 下午4.05.19.png")

eval方法里面传的参数加入括号'()'的原因:

由于json是以{}的方式来开始以及结束的,在JS中,它会被当成一个语句块来处理,所以必须强制性的将它转换成一种表达式。

加上圆括号的目的是迫使eval函数在处理JavaScript代码的时候强制将括号内的表达式(expression)转化为对象,而不是作为语句(statement)来执行。参考链接:JSON.parse与eval的区别

第6章 面向对象的程序设计

es5 es6类和实例化学习备忘

第7章 函数表达式

重要概念:用var functionName = function(arg0,arg1){}创建了一个函数把它赋值给functionName变量。这样创建的函数是匿名函数

递归

给出了一个例子,这个函数的问题在于anotherFactorial指向了之前factorial指向的函数,把factorial设为null,就只有anotherFactorial指向原来的函数了,关键是在这个函数内部用到了factorial这个函数名,而此时factorial已经不再指向函数。

function factorial(num) {     
    if (num <= 1){         
        return 1     
    } else {         
            return num * factorial(num-1); //factorial已经不再指向函数 
    } 
}
var anotherFactorial = factorial; 
factorial = null; 
console.log(anotherFactorial(4));

这个问题有两种解决办法:

  1. 使用arguments.callee代替函数名,就不会出现这种状况了,但是在严格模式下,不能访问arguments.callee这个属性。
  2. 使用命名函数表达式,在严格模式和非严格模式下都没有问题。然而什么是命名函数表达式?

之前说用var functionName = function(arg0,arg1){}创建的函数是匿名函数,也就是说这个函数是没名字的。如果在表达式右边的的function后面加上一个名字f。这样就给这个匿名函数命名了,不过这个名字仅仅在f函数本身的内部作用域内有效。

var factorial = function f(){     
 return typeof f; // f是在内部作用域内有效   
};   
// foo在外部用于是不可见的   typeof f; 
// "undefined"   factorial(); 
// "function"

书上给出的解决示例是这样的:还给表达式右边的加上了一个括号(),不过试了下加不加括号结果都是一样的,~~可能这样写是为了好看。~~加括号是为了表示它实际上是一个函数表达式。

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

闭包

闭包的概念:有权访问另一个函数作用域中的变量的函数

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

闭包的原理:一般情况下。函数执行完毕之后,局部活动对象就会被销毁,内存中仅保存全局执行环境的变量对象。但闭包不同,因为在函数内部定义的函数会把外部函数的活动对象添加到自己的作用域链上.

function outerFunc(outer) { 	
    return function (inner){ 		
    console.log(outer); 	
    }
} 
var innerFunc = outerFunc("haha");//匿名函数从outerFunc()中被返回后,它的作用域链初始化为包含outerFunc函数的活动对象和全局变量对象。所以outerFunc函数在执行完毕后,其活动对象也没有被销毁,而是被匿名函数的作用域链继续引用了。 
innerFunc(); //"haha" 内部的匿名函数可以访问到外部函数outerFunc中的变量outer。 innerFunc = null;
//匿名函数销毁之后,outerFunc函数的活动对象才会被销毁

闭包与变量

闭包只能取得外部函数中任何变量的最后一个值,这句话的意思可以用for循环的例子说明:

function createFunc(){ 	
    var result = new Array(); 	
        for (var i = 0;i < 4;i++) { 		
            result[i] = function() { 			
                return i; 		
            };    
        }     
    return result; 
} 
var innerFuncList = createFunc();//返回一个函数数组[ [Function], [Function], [Function], [Function] ]
var innerFunc1 = innerFuncList[0]; 
var innerFunc4 = innerFuncList[3]; 
console.log(innerFunc1(),innerFunc4()); // 4 4

本来设想的是每个内部函数返回自己对应的索引值。但实际上他们都返回最后的索引值4。因为这些内部函数都保存着外部函数的活动对象,所以它们引用的是同一个变量i,外部函数返回后,变量i的值是4.

可以通过对result[i]赋值这段进行改写,达到预期效果:

result[i] = function(num){ 	
    return function(){ 		
        return num; 	
    } 
}(i);

等号右侧设置了一个立即执行的匿名函数,关键是把变量i传给参数num了,而函数参数是按值传递的。所以每次num拿到的都是i的当前值。

闭包的this对象

注意:匿名函数的执行环境具有全局性,所以其this对象指向window。

为什么匿名函数取得的不是其包含作用域的this对象?

每个函数在被调用的时候都会自动取得两个特殊变量:this和arguments。但内部函数在搜索这两个变量时,只会搜索到其活动对象为止。不会去搜索外部函数中的这两个变量。

模仿块级作用域

立即执行函数第一个括号表示它实际上是一个函数表达式,紧随其后的第二个括号表示立即调用这个函数。不写第一个括号是会报错的,因为javascript会把function关键字当做一个函数声明的开始,而函数声明后面不能跟圆括号。

function(){ 
    // ... 
 }(); // 报错 
 
function foo(){ 
    //	... 
}(); // 报错  

(function(){ 
    // ...
})();// 不报错