JavaScript系列----函数(Function)篇(4)
1.什么是函数?
在W3C中函数的定义是这么说的:函数是由事件驱动的或者当它被调用时执行的可重复使用的代码块。
诚然,从这种抽象的定义中我们得不到什么有价值的东西。下面,举例来列举出函数的几种定义方式:
function add(num1, num2) {
return num1 + num2;
}
var add = function (num1, num2) {
return num1 + num2;
}//这是比较常见的两种 //下面两种比较少见
var add=new Function("num1","num2","return num1+num2");
var add=Function("num1","num2","return num1+num2");
上面四种写法均是创建一个函数正确的语法。但是,常见的一般是前两种。因为相比于前两种,后两种存在着一些缺陷。
- 后两种比较繁琐不直观。这点从上面的例子中可以看出。
- 后两种存在着一些致命的缺陷。这种函数的创建方式,不能维持一个属于函数的作用域链,在任何时候下(new)Function的创建的函数,都相当于在全局作用域下创建的函 数。以下例子就可以证明这一点。
var x = 1;
var add = (function () {
var x = 100;
return function (num) {
return num + x;
}
}());
console.log(add(0));//100var x = 1;
var add = (function () {
var x = 100;
return new Function('num1', 'return num1+x');
}());
console.log(add(0));//1也就是说,后两种方式创建的函数,不能组成完整的函数作用域链(后面会讲到),也就不可能有所谓的闭包之说。
- 后两种的运行效率太低。
首先,JS对字符串的解析算不上效率很高,而(new)Function均存在着大量的字符串。
其次,JS的解释器,对于用function(){},这种形式的函数,都有一定形式的优化。比如下面这样var array = [
];
for (var i = 0; i < 1000; i++) {
array[i] = function () {
return 'undefined';
}
}//第一种var array = [
];
for (var i = 0; i < 1000; i++) {
array[i] = new Function("return undefined");
}//第二种,
这两种方式在运行效率上存在着很大的差距。对于,第一种只需要执行一次function(){},其他的999次都是赋值,而后一种要执行一千遍的函数创建并赋值。
正是因为前面的三种原因,才使得function(){}这种方式比较流行。
另外,你可能也见过下面的这种,但是这种形式只是一种变体。
var add = new function () {
return 1 + 2;
};
console.log(typeof add);//object
var result = add.constructor();/*调用时必须采用这种调用方式*/
console.log(result);//3
这种形式,new function()创建的实质上是利用一个匿名函数创建一个对象。这个对象的一个constructor属性正好指向其构造函数,也就是这个匿名函数。所以实际上这是一种丑陋的写法。
到这里,我们也就只是叙述了一下,定义函数的几种方式。通过比较,我们知道前两种比较实用,但是即使这样,第一种和第二中的定义方式也存在着巨大的不同。下一小节,我们接着讲这两种方 式存在的差异。
2.函数声明和函数表达式
函数声明:
function 函数名称 (参数:可选){ 函数体 }
函数表达式:
function 函数名称(可选)(参数:可选){ 函数体 }
所以,可以看出,如果不声明函数名称,它肯定是表达式,可如果声明了函数名称的话,如何判断是函数声明还是函数表达式呢?ECMAScript是通 过上下文来区分的,如果function foo(){}是作为赋值表达式的一部分的话,那它就是一个函数表达式,如果function foo(){}被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。
所以,我们可以看出,在第一部分的前两种创建函数的方式分别为函数声明和函数表达式。
function add(num1, num2) {
return num1 + num2;
}//函数声明
var add = function (num1, num2) {
return num1 + num2;
}//函数表达式
另外,还有一些比较容易和函数声明混淆的函数表达式。
(function add(num1, num2) {
return num1 + num2;
});//函数表达式
var add = function foo(num1, num2) {
return num1 + num2;
}//函数表达式
()在JS语言规则中是一个分组操作符,根据W3C标准分组操作符里面的会默认为是表达式。
而下面一种则比较有意思,赋值表达式的左边是一个函数,关于这点不同的解析器对此的处理不同,有的认为这是函数声明,有的认为这是一个函数表达式,不同的解析器对此的处理各不相同。
但是目前在主流浏览器上默认的是函数表达式,而且foo作为函数标识符,只在其函数内部能被识别。看下面的例题:
var add=function foo(n){
if(n==1)return 1;
else return n+foo(n-1);
}; console.log(add(3));//6
console.log(foo(3));//error, foo is not defined
那么函数声明和函数表达式有什么区别呢?
回答这些问题,就涉及到函数被调用时的情况了。
3.函数的执行环境和作用域
我们都知道,函数运行时是运行在新开辟的栈里的。那么函数在运行时,代码的执行环境是什么样的呢?
函数被调用时发生了什么?
function add(num1, num2) {
var num3 = 300;
return num1 + num2 + num3;
}
var result = add(100, 200);
console.log(result);
这一段代码在执行到第5行的时候会调用我们声明的add函数,add函数在被调用时会做以下处理:
1.add函数形参的声明并赋值。
2.add函数内函数的声明(若函数变量与函数形参的变量同名,则函数声明会覆盖形参声明)。
3.add函数内变量的声明。----->函数按顺序执行。
下面的几个例子可以证明:
例题一:函数内的函数的声明会覆盖函数形参的声明。
function add(num1, num2) { console.log(typeof num1);//function
function num1() {}
}
var result = add(100, 200);
例题二:函数内变量的声明不会覆盖形参的声明和函数内函数的声明
function add(num1, num2) {
console.log(typeof num1);//number
var num1="23"; }
var result = add(100, 200);
补充:
所谓变量的声明都是类似 var x, y; 这种情况。而var x=1;
其实是 var x ; x=1;分两步执行。
变量声明总是这样: var x;
函数的执行环境:
由上述我们知道,函数执行的时候开辟一个新的栈,而栈内保存着函数内部声明的变量,变量的值在函数代码运行之前按照刚才所讨论的三步赋值。也就是说,当一个函数被调用时,在代码运行之前其栈中已经存在着函数运行时所需的所有的变量。这些变量加一起则构成函数的执行环境。
如果解释器真的把函数内部所有的声明都放在栈中的话,那么解释器在开辟栈的时候就应该可以确定所开辟栈空间的大小。但是如果栈空间大小确定以后,有以下几个问题就需要解决了:
- 变量的类型在运行时被改变。此时栈空间的大小需要不断调整。
- 每个函数在声明的时候会维持一个属于自己的作用域链,如果作用域链上的变量所占用的空间大小改变的话,需要对整个作用域链上的栈调整。
- 需要针对这种结构重写垃圾回收机制(标记--清理不适用了)。
改进:引进函数变量
当函数被调用的时候,并非将变量的声明保存在栈中,而是保存在一个对象中。而将这个对象的引用保存在栈中。而这个对象存储在堆中,具体的工作原理如下:
function add(num1, num2) {
var num3 = 300;
return num1 + num2 + num3;
}
var result = add(100, 200);
当函数被调用时(未执行之前),解释器创建一个addReference对象:
addReference={
num1:100;
num2:200;
num3:undefined;
};
addReference对象的引用被压入栈中,而对象本身则存在于堆中。
补充:
函数在运行时,栈中还保存着返回值以及this指针。在函数执行完毕退出时,会清空栈,若存在对此函数变量引用的函数,则将此函数变量加入引用函数的作用域链上,否则过一段时间,若垃圾回收机制为未发现有此函数变量的引用,则将该函数变量删除。
改进后:
- 栈空间大小确定: 函数运行之前,栈所需要的空间已经能被确定。只存在三个元素: this指针,函数变量的引用,返回值。(返回值也保存在一个变量中,由解释器管理)
- 函数在运行时,若创建一个新的函数。则只需要将函数变量对象加入新函数的作用域链上即可。
- 只需要根据此函数变量的引用计数是否为0就可以管理内存,而不需要重写垃圾回收机制。
4.函数的作用域链
在第三部分我们讨论出,函数的作用域上保存的都是函数变量。下面我们通过这个例子来说明这种现象。
var fun;
(function fun1() {
var x = 1;
(function fun2() {
var y = 2;
fun = function () {
return x + y;
}
}());
}()) var result=fun();
console.log(result)//
根据上例,我们来一步步分析函数执行时都发生了什么?
- 在全局作用域中的变量对象。
globalReference = {
.....//以前存在的对象 比如 Object,Math,Date之类
fun: undefined;
result: undefined;
} - 当函数运行至第二行时,fun1的变量对象
fun1Reference = {
x: undefined;
} //执行至第4行的时候,
fun1Reference = {
x: 1;
}
//fun1的作用域链: globalReference - 当函数运行至第四行时,fun2的变量对象
fun2Reference = {
y: undefined;
}//执行至第5行的时候,
fun2Reference = {
y: 2;
}//fun1的作用域链: globalReference--->fun1Reference
- 当函数运行至第6行时,fun的作用域链
//fun的作用域链: globalReference--->fun1Reference--->fun2Reference
- fun2执行完毕退栈--->fun1执行完毕退栈。
- 当函数运行至第12行时,fun被调用。
globalReference = {
| .......
| fun: undefined;
| result: undefined;
| }
fun1Reference ={x:1;}
|
|
|
|
fun2Reference={y:2}
|
|
|
|
funReference:{}; - fun执行完毕,result=6;13行,输出结果。
上述,我们已经模拟一遍函数的执行时的过程。下面我们来介绍一下全局作用域对象。
首先,先明确一点,全局作用域也是一个变量对象。
globalReference = {
Object:内置的对象构造函数对象;
Array:内置的数组构造函数对象;
Function:内置的函数构造函数对象;
Math:内置的Math对象;
Date:内置的日期构造函数对象;
.......;
window:globalReference
}
window对象保持这对全局对象的引用。
补充:在控制台下执行的代码都是在eval()函数中执行的,这个函数能够使用并且能改变当前函数所在的执行环境。
5.变量和属性的区别
1、变量:可以被改变的量。只有前面加var(非函数)的才能称为变量。函数变量有自己独特的变量声明方式。
var x = 1,y = 2;
var z = 3;
//类似上面这种才是变量 xxx=1;//这样的不是变量,下面会讲到这种形式
2、属性:一个对象内的变量。
var object={
x:1,
y:2,
z:3
}; //x,y,z均为属性。
上面的两种都很容易区分,但是下面这种又该如何解释呢?
rest=1;//rest是属性还是变量?
这句话一般是在函数执行时候,经常性遇到,这样写有很大的弊端。
- 查找较慢。rest前面没有var,则肯定不是变量。那么,在执行的时候就会沿作用域链一直向上查找,直至到全局作用域中的变量对象。此时未找到,则根据规则将其作为全局作用域变量对象的属性。
2.改变了全局作用域变量对象。一般来说,我们在执行代码的时候应该尽量避免改变全局作用域对象。
也就是说,如果我们使用一个前面没有加var的“变量”,则在执行期间,会将该“变量”当做全局作用域变量的属性。
3、变量和属性的区别:
- 属性(配置为可删除的情况下)可以通过其所在的对象被删除。
比如:
var object={
x:1
}
delete object.x; //true
y=2;
delete window.y(或者delete y);//true
- 变量在声明的时候会被作为函数变量的属性。原则上也是可以被删除,但是因为我们不能得到函数变量这个对象(window是一个特例),所以在实际操作中,也就导致不可能被删除。
- 变量一般是针对声明时期,而属性一般针对执行时期。两者在本质上,意义就不一样。
- 查找普通对象的属性,如果未找到不会抛出错误。但是,查找变量对象的属性,如果未找到则会抛出错误。
var object = {
};
console.log(object.z);//undefined,-----在普通变量中查找
console.log(window.v);//undefined -----在普通变量中查找
console.log(z);//error; -----在作用域链上的变量对象中查找,未找到则报错。
JavaScript系列----函数(Function)篇(4)的更多相关文章
- 马上运行函数-$(function(){})篇
QQ:1187362408 欢迎技术交流和学习 马上运行函数-$(function(){})篇(jquery): TODO: 1.jquery:jQuery(function($){ }) 与 $(d ...
- JavaScript入门-函数function(二)
JavaScript入门-函数function(二) 递归函数 什么是递归函数? 递归简单理解就是,在函数体里,调用自己. //我们在求一个10的阶乘的时候,可能会这么做 //写一个循环 var to ...
- javaScript的函数(Function)对象的声明(@包括函数声明和函数表达式)
写作缘由: 平时再用js写函数的时候,一般都是以惯例 function fn () {} 的方式来声明一个函数,在阅读一些优秀插件的时候又不免见到 var fn = function () {} 这种 ...
- 深入理解JavaScript系列(结局篇)
介绍 最近几个月忙得实在是不可开交,终于把<深入理解JavaScript系列>的最后两篇“补全”了,所谓的全是不准确的,因为很多内容都没有写呢,比如高性能.Ajax安全.DOM详解.Jav ...
- JavaScript中函数function fun(){}和 var fun=function(){}的区别
function fun(){} 和 var fun=function(){}的区别 标题有点长···· 废话少说,其实他们的主要区别就是"函数声明的提前行为". var fun= ...
- Javascript基础--函数(Function对象)
1.函数是一段可执行的代码,函数可多次调用,模块化管理. 2.使用function语句,function funName([arg1][,arg2]....[,argn]){代码块}.所有版本可用,一 ...
- 深入理解JavaScript系列
转自http://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html 深入理解JavaScript系列(1):编写高质量JavaScript代码 ...
- 深入理解JavaScript系列(转自汤姆大叔)
深入理解JavaScript系列文章,包括了原创,翻译,转载,整理等各类型文章,如果对你有用,请推荐支持一把,给大叔写作的动力. 深入理解JavaScript系列(1):编写高质量JavaScript ...
- [转]深入理解JavaScript系列
文章转自:汤姆大叔-深入理解JavaScript系列文章 深入理解JavaScript系列文章,包括了原创,翻译,转载,整理等各类型文章,如果对你有用,请推荐支持一把,给大叔写作的动力. 深入理解Ja ...
随机推荐
- 如何使用git 发布源码到CodePlex
github 是分布式源码管理系统 codeplex 是微软的开源社区 将git中源码分享到codeplex社区其实很方便,按照如下步骤: 1:注册codeplex 帐号或使用微软的已有的帐号 2:下 ...
- 【转】python数据格式化之pprint
pprint – 美观打印 作用:美观打印数据结构 pprint 包含一个“美观打印机”,用于生成数据结构的一个美观视图.格式化工具会生成数据结构的一些表示,不仅可以由解释器正确地解析,而且便于人类阅 ...
- 选择排序的3种语言实现方法(C java python)
1.选择排序的思路是:遍历数组,第一遍找出所有成员的最小值,放到数组下标为0的位置,第二遍从剩余内容中,再次找出最小值,放到数组下标为1的位置,以此类推,遍历完成所有的数组内容,最后结果就是:数组是按 ...
- iOS多线程基本使用
大家都知道,在开发过程中应该尽可能减少用户等待时间,让程序尽可能快的完成运算.可是无论是哪种语言开发的程序最终往往转换成汇编语言进而解释成机器码来执行.但是机器码是按顺序执行的,一个复杂的多步操作只能 ...
- migo的增强
migo的增强 所用BADI:MB_MIGO_BADI 具体见例子:(SE19) CIN_PLUG_IN_TO_MIGO JVA_CRP_MIGO_BADI JV_CRP_MIGO_BADI WB ...
- 张高兴的 Xamarin.Android 学习笔记:(三)活动生命周期
本文将直接解释我写的一个示例.示例目的在于展示 Android 活动在 Xamarin 中的用法.如果有朋友对基础知识不太了解建议先学 Android . 新建一个 Xamarin.Android 项 ...
- 模块:time,random,os,sys
时间模块 import time # print(time.time()) #时间戳 # print(time.strftime('%Y-%m-%d %X')) #格式化字符 # print(time ...
- coursera_poj_魔兽世界终结版
五个下午的时间!!!!终于过了!!有史以来做的最复杂的一个题这是我迄今为止做的最复杂也最具有挑战的一个oj作业.虽然之前做过比这个规模一些作业项目,但是往往有简单的模块框架,模块之前的关系也只是有些简 ...
- redis数据类型和应用场景
Redis是什么?两句话可以做下概括: 1. 是一个完全开源免费的key-value内存数据库 2. 通常被认为是一个数据结构服务器,主要是因为其有着丰富的数据结构 strings.map. list ...
- Oracle学习笔记之存储过程
...