1. 函数定义
2. 函数保存
3. 函数创建
4. JS预解析,函数参数变量提升申明
5. 函数执行环境和作用域
6. 函数内部属性 arguments 和 this
7. 闭包
1. 函数定义:函数也是一种对象,内置的Function()函数用于创建函数对象实例。
2. 函数保存 函数名:是指向函数对象的引用类型变量,函数名变量保存在栈内存,函数对象保存在堆内存。
3. 函数创建: new构造函数 和 关键字function(函数声明和函数表达式)
3.1 new构造函数
3.2 关键字function(函数声明和函数表达式)
4. JS预解析,函数参数变量提升申明
4.1 函数申明和函数表达式两者如何区分:如果没有函数名,则一定是函数表达式。其它的主要根据两者上下文区分。
4.2 函数申明和函数表达式两者区别:在JS引擎解析的时候,函数申明会提升,而函数表达式不会。
JS预解析,函数变量声明过程:
1、引擎在解析时,首先会解析函数(function关键字)声明,然后再解析变量(var 关键字)声明并赋值为undefined。
遇到重名函数和变量,无论先后顺序,都只保存函数。相同变量或函数会保留后面的。所有的函数在正式运行代码之前,都只是函数块保存在内存代码区中。
2、当逐行执行代码时,表达式会改变内存中预解析变量的值。
3、当逐行执行代码,调用执行函数时,则会开辟新的函数域,又会执行预解析和逐行执行命令过程。
首先内部函数声明提升,并将函数名的类型设置为函数类型,然后解析函数参数,将传入的实际参数值赋给形式参数,最后再内部变量声明提升,只提升声明,不初始化,如果有重名,同优先级的后面覆盖前面的,不同优先级的不覆盖(已经解析了优先级高的,就不再解析优先级低的).
4、在全局作用域里,页面里有多对<script>代码块,会先解析和执行完前面的<script>后才会预解析和执行后面的<script>,但是前面预解析保存的变量和函数,后面的script中也可以使用。
全局作用域
函数作用域:函数作用域在函数定义时不存在的,只有在函数实际调用才有函数作用域。
4.3 作为值的函数
在一般的编程语言中,如果要将函数作为值来使用,需要使用类似函数指针或者代理的方式来实现,但是在ECMAScript中,函数是一种对象,拥有一般对象具有的所有特征,可以做为一个引用类型的值去使用,比如函数也可以作为另一个函数的参数或者返回值,异步处理中的回调函数就是一个典型的用法。
4.4 函数预解析到相同名字函数时:重载
函数是对象,函数名是指向函数对象的引用类型变量,不能实现重载。
在ECMAScript中,怎么模拟重载呢?
简单数据类型包装对象(Boolean、Number、String),既可以作为构造函数创建对象,也可以作为转换函数转换数据类型,这是一个典型的重载。
5. 函数执行环境和作用域
5.1 执行环境(execution context):所有的JS代码都运行在一个执行环境中。活动的执行环境从逻辑上形成了一个栈,全局执行环境永远是这个栈的栈底元素,栈顶元素就是当前正在运行的执行环境。每一个函数都有自己的执行环境,当执行流进入一个函数时,会将这个函数的执行环境压入栈顶,函数执行完之后再将这个执行环境弹出,控制权返回给之前的执行环境。
5.2 变量对象(variable object):每一个执行环境都有一个与之对应的变量对象,执行环境中定义的所有变量和函数就是保存在这个变量对象中。
5.3 作用域链(scope chain):当代码在一个执行环境中运行时,会创建由变量对象组成的一个作用域链。这个链的前端,就是当前代码所在环境的变量对象,链的最末端,就是全局环境的变量对象。在一个执行环境中解析标识符时,会在当前执行环境相应的变量对象中搜索,找到就返回,没有找到就沿着作用域链一级一级往上搜索直至全局环境的变量对象,如果一直未找到,就抛出引用异常。
5.4 活动对象(activation object):如果一个执行环境是函数执行环境,也将变量对象称为活动对象。活动对象在最开始只包含一个变量,即arguments对象(这个对象在全局环境的变量对象中不存在)。
当前执行环境被弹出栈就会被销毁,当 当前执行环境恢复成全局环境时整个处理过程结束,全局环境直至页面退出再销毁。
对于作用域链,还可以使用with、try-catch语句的catch块来延长:
- 使用with(obj){}语句时,将obj对象添加到当前作用域链的最前端。
- 使用try{}catch(error){}语句时,将error对象添加到当前作用域链的最前端。
函数在内部递归调用自己的实现原理:就是作用域链。
函数名是函数定义所在执行环境相应变量对象的一个属性,然后在函数内部执行环境中,就可以沿着作用域链向外上溯一层访问函数名指向的函数对象了。
如果在函数内部将函数名指向了一个新函数,递归调用时就会出问题。
6. 函数内部属性 arguments 和 this
函数内部属性:可以理解为函数相应的活动对象的属性,是只能从函数体内部访问的属性,函数每一次被调用,都会被重新指定,具有动态性。this和arguments。
6.1 arguments对象(这个对象在全局环境的变量对象中不存在)。
1_ECMAScript 里函数形参与实际传入的实参两者之间数量没有任何限制。
2_形式参数甚至可以取相同的名称,只是在实际传入时会取后面的值作为形式参数的值(这种情况下可以使用arguments来访问前面的实际参数)。
3_实际接收的参数组成arguments对象.类数组对象。 访问传入参数, 第一个参数arguments[0], 第二个参数arguments[1],
4_arguments属性:length、callee和caller
1、length属性表示实际接收到的参数个数
2、callee属性指向函数对象本身,即有: fn.arguments.callee === fn
3、caller属性主要和函数的caller相区分,值永远都是undefined
5_arguments保存实际参数值,而形式参数也保存实际参数值,这两者之间有一个同步关系,修改一个,另一个也会随之修改。
6_arguments和形式参数之间的同步,只有当形式参数实际接收了实际参数时才存在,对于没有接收实际参数的形式参数,不存在这种同步关系。
7_arguments对象虽然很强大,但是从性能上来说也存有一定的损耗,所以如果不是必要,就不要使用,建议还是优先使用形式参数。
函数内部调用自身时与函数名解耦 (可以使用arguments.callee(在ES5的严格模式下被禁止使用) 和 匿名的函数表达式)
6.2 this
1_全局环境中的this
在全局环境中,this指向全局对象本身(浏览器中也就是window),这里也可以把全局环境中的this理解为全局执行环境相应的变量对象,在全局环境中定义的变量和函数都是这个变量对象的属性:
7. 函数属性和方法 函数内部属性
函数是一个对象,因此也可以有自己的属性和方法。不过函数属性和方法与函数内部属性很容易混淆,对比学习。
(1)函数内部属性:可以理解为函数相应的活动对象的属性,是只能从函数体内部访问的属性,函数每一次被调用,都会被重新指定,具有动态性。this和arguments。
(2)函数属性和方法:这是函数作为对象所具有的特性,只要函数一定义,函数对象就被创建,相应的属性和方法就可以访问,并且除非你在代码中明确赋为另一个值,否则它们的值不会改变,因而具有静态性。有一个例外属性caller,表示调用当前函数的函数,也是在函数被调用时动态指定。在ES5的严格模式下,不能对具有动态特性的函数属性caller赋值。
类别 | 名称 | 说明 | 是否来自Object继承 |
函数内部属性 | arguments | 函数实际参数的类数组对象 | 不谈继承性 |
this | 函数据以执行的环境对象 | 不谈继承性 | |
函数属性 | caller | 调用当前函数的函数 | 否 |
length | 函数形式参数的长度 | 否 | |
prototype | 函数原型对象 | 否 | |
constructor | 表示创建函数实例的函数即Function() | 继承了Object方法 | |
函数方法 | call | 动态绑定函数内部属性this, 以列举方式接收参数,会立刻执行 | 否 |
apply | 动态绑定函数内部属性this, 以数组方式接收参数,会立刻执行 | 否 | |
bind | 动态绑定函数内部属性this, 以列举方式接收参数,需要的时候调用执行 | 否 ES5中新增 | |
hasOwnProperty(propertyName) | 检查给定的属性是否在当前对象实例中 | 继承了Object方法 | |
propertyIsEnumerable(propertyName) | 检查给定的属性是否能够是使用for-in语句来枚举 | 继承了Object方法 | |
isPrototype(object) | 检查传入的对象是否是另一个对象的原型 | 继承了Object方法 | |
toString() | 返回对象的字符串表示 | 覆盖了Object类型方法 | |
valueOf() | 返回对象的字符串、数值或布尔值表示,通常与toString()方法返回值相同 | 覆盖了Object类型方法 | |
toLocalString() | 返回对象的字符串表示,该字符串与执行环境的地区相对应 | 覆盖了Object类型方法 |
使用最多的是函数本身特有的方法:call(),apply()和bind(). 设置函数内部属性this从而扩展函数作用域。
7. 闭包
闭包是指有权访问另一个函数作用域中的变量的函数。对象是带函数的数据,而闭包是带数据的函数。
实例入门:普通情况下全局作用域下无法直接访问局部作用域变量。
function fn1() { var i = 10; } fn1(); //函数执行完毕,函数作用域被弹出JS作用域,再加上垃圾回收机制,i会被回收 alert ( i ); //提示错误,i未定义 i是局部变量,也无法在外部直接调用。
简单闭包:
function fn1(){ var i = 10; function fn2(){ alert(i); //fn2函数内引用外部fn1函数的变量i,所以变量i也被引用着,也不会被垃圾回收机制回收。 } return fn2; //fn1函数内部定义的fn2函数,并作为返回值 } var test = fn1(); //fn1调用执行返回fn2,并且被全局变量test引用,所以,fn2函数不会被垃圾回收机制回收。 test(); //test保存了fn2的函数地址,所以test()可以直接被执行,而且作用域链也可以访问到其所调用的变量,所以最后可以输出10.
作用域链从内到外,闭包从外到内
闭包主要用途:
//1. 保存临时数据 //2. 修改this指向 //3. 进行缓存 //4. 模仿块级作用域 //5. 模仿私有变量和私有函数、模块模式等
1.利用闭包保存数据
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(){ return i; }; } return result; } var funcs = createFunctions(); for (var i=0,l=funcs.length; i < l; i++){ console.info(funcs[i]()); //会输出10个10, } //由于闭包带有的数据是createFunctions相应的活动对象的最终状态,而在createFunctions()代码执行完成之后,活动对象的属性 i 已经变成10, //因此在下面的调用每一个函数,都去寻找内存中的i,i最后状态都是10,所以都输出10了 // //要处理这种问题,可以采用匿名函数作用域来保存状态: function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = (function(num){ //每一个状态都使用一个立即调用的匿名函数来保存(保存在匿名函数相应的活动对象中), return function(){ //然后在最终返回的函数被调用时,就可以通过闭包带有的数据(相应的匿名函数活动对象中的数据)来正确访问了 return num; }; })(i); } return result; } var funcs = createFunctions(); for (var i=0,l=funcs.length; i < l; i++){ console.info(funcs[i]()); //输出0,1,2,3,4,5,6,7,8,9 } //尽管闭包存在效率和内存的隐患 //闭包在性能上会有较大影响,因此建议不要滥用, //由于闭包会保存其它执行环境的活动对象作为自身作用域链中的一环,这也可能会造成内存泄露。
var fibonacci = (function(){ //使用闭包缓存,递归 var cache = []; function f(n){ if(1 == n || 2 == n){ return 1; }else{ cache[n] = cache[n] || (f(n-1) + f(n-2)); //使用闭包缓存,递归 ,如有缓存直接使用 return cache[n]; } } return f; })(); var f2 = function(n){ //不使用闭包缓存,直接递归 if(1 == n || 2 == n){ return 1; }else{ return f2(n-1) + f2(n-2); //不使用闭包缓存,每次都会计算 } }; //测试两者的计算时间 var test = function(n){ var start = new Date().getTime(); console.info(fibonacci(n)); console.info(new Date().getTime() - start); start = new Date().getTime(); console.info(f2(n)); console.info(new Date().getTime() - start); }; test(10);//55,2 ,55,2 test(20);//6765,1 ,6765,7 test(30);//832040,2, 832040,643 //n值越大,使用缓存计算的优势越明显
4.利用闭包模仿块级作用域
(function(){ //这里是块语句 //这种模式也称为 立即调用的函数表达式, //特别是由于jQuery源码使用这种方式而大规模普及起来。 })();