javascript 函数及作用域总结介绍_javascript技巧_脚本之家

2019-12-04 06:07栏目:竞技宝竞猜
TAG:

闭包是JavaScript中一个重要的特性,其最大的作用在于保存函数运行过程中的信息。在JavaScript中,闭包的诸多特性源自函数调用过程中的作用域链上。

JS的闭包真的是一个老生常谈的知识点了,无奈它并不是那么好掌握,但是它又是那么重要,很多高级应用的开发都会用到闭包去解决相关问题。希望你能从这篇文章了解基本的闭包知识点,后期会不定期更新使之更加完善。

在js中使用函数注意三点:1、函数被调用时,它是运行在他被声明时的语法环境中的;

函数调用对象与变量的作用域链

在了解闭包知识以前,先说下函数作用域问题。在一些类似C语言的编程语言中,它们是具有块级作用域(block scope)的,但是在JavaScript中却没有块级作用域,取而代之的是函数作用域(function scope):“变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的”。(引用自《JavaScript权威指南》)

2、函数自己无法运行,它总是被对象调用的,函数运行时,函数体内的this指针指向调用该函数的对象,如果调用函数时没有明确指定该对象, this 默认指向 window ( strict 模式除外,本文不涉及 strict 模式);

对于JavaScript中的每一次函数调用,JavaScript都会创建一个局部对象以储存在该函数中定义的局部变量;如果在该函数内部还有一个嵌套定义的函数,那么JavaScript会在已经定义的局部对象之上再定义一个嵌套局部对象。对于一个函数,其内部有多少层的嵌套函数定义,也就有多少层的嵌套局部对象。该局部对象称为“函数调用对象”(ECMAScript 3中的“call object”,ECMAScript 5中改名为“declarative environment record”,但个人认为还是ECMAScript 3中的名称更容易理解一些)。以下面的函数调用为例:

例如如下代码:

3、函数是一种带有可执行代码的对象类型数据。

复制代码 代码如下:function f{ var a = 10; return a*x;}console.log;//60

function test(o) {

一、声明函数

在这个简单的例子中,当调用f()函数时,JavaScript会创建一个f()函数的调用对象,在f_invokeObj对象内部有两个属性:a和x;运行f()时,a值为10而x值为6,因此最后的返回结果为60。图示如下:

var i = 0;    // i在整个函数体中都是有定义

1、使用 function 关键字复制代码 代码如下:function myfun{ //声明名为myfun的函数

当存在函数嵌套时,JavaScript将创建多个函数调用对象:

if(typeof o == "object") {

return a+b;

复制代码 代码如下:function f{ var a = 10; return a*g{ return b*b; }}console.log;//360

var j = 0;    // j在函数体中是有定义的,不仅仅是在这段代码内

} 2、 声明匿名函数

在这个例子中,当调用f()函数时,JavaScript会创建一个f,其内部有两个属性a和x,a值为10而x值为6;运行f函数中的g的调用对象,其内部有一个属性b,b值与传入参数x相同为6,因此最后的返回结果为360。图示如下:

for(var k = 0; k < 10; k++) {  // k在函数体内是有定义的,不仅仅是在循环内

function{ return a+b;}匿名函数自身是无法保存的,由于在js中函数是一种对象型数据,因此可以把匿名函数赋给变量来保存。

可以看到,函数调用对象形成了一条链。当内嵌函数g()运行,需要获取变量值的时候,会从最近的函数调用对象中开始进行搜索,如果无法搜索到,则沿函数调用对象链在更远的调用对象中进行搜寻,此即所谓的“变量的作用域链”。如果两个函数调用对象中出现相同的变量,则函数会取离自己最近的那个调用对象中的变量值:

console.log(k);    // 输出数字0-9

var myfun = function{ return a+b;}3、使用函数构造器Function //注意首字母大写

复制代码 代码如下:function f{ var a = 10; return a*g{ var a = 1; return b*b*a; }}console.log;//360, not 3600

}

Function 是js内置的一个函数,他是所有函数对象的构造器。(其他数据对象也有自己的内置构造函数,比如Number,Object等,这些构造函数自己的构造器就是Function,因为他们都是函数)。

在上面的例子中,g和f中均存在变量a且a的值不同,当运行g函数内部所使用的a值为1,而在g()函数外部所使用的a值则为10。图示此时的函数调用对象链如下:

console.log(k);    // k已经定义了,输出数字10

var myfun = new Function; 其中最后一个参数是函数体,前面的参数都是函数的形式参数名,个数不定,因为需要用字符串传参来构造,函数较长时这种写法很不方便,一般很少用,也许你会用它来构造特定的返回值从而取代 eval函数。

什么是闭包?

}

需要注意的是,全局变量和全局函数都可以看作window对象的属性,如果存在同名的函数和变量,只能有一个生效,试试下面的代码。复制代码 代码如下:function a;}

在JavaScript中所有的函数都是对象,而定义函数时都会产生相应的函数调用对象链,一次函数定义对应一个函数调用对象链。只要函数对象存在,相应的函数调用对象就存在;一旦某函数不再被使用,相应的函数调用对象就会被垃圾回收掉;而这种函数对象和函数调用对象链之间的一一组合,就称之为“闭包”。在上面f函数的例子中,就存在两个闭包:f()函数对象和f_invokeObj对象组成了一个闭包,而g()函数对象和g_invokeObj-f_invokeObj对象链一起组成了第二个闭包。当g函数不再被使用,因此g函数执行完毕后,由于同样的原因,f()闭包也被垃圾回收了。

console.log(j);    // j已经定义了,但是可能没有初始化

alert; //访问window对象的属性也可以省去window不写

从闭包的定义可以得出结论:所有的JavaScript函数在定义后都是闭包 – 因为所有的函数都是对象,所有的函数在执行后也都有其对应的调用对象链。

}

var a=1;

不过,令闭包真正发挥作用的是嵌套函数的情况。由于内嵌函数是在外部函数运行的时候才开始定义的,因此内嵌函数的闭包中所保存的变量值是这次运行过程中的值。只要内嵌函数对象依然存在,那么其闭包就依然存在,从而也就实现了保存函数运行过程的信息这个目的。考虑以下这个例子:

上面这段代码中,如果我们传入不同的参数,j的打印结果是不一样的。比如:

alert; 函 数和变量的声明都发生在代码解析期,不同的是,变量在解析期只声明不赋值,因此,同一个作用域内存在同名的函数和变量时,在代码运行期执行到变量赋值之 前,同名函数生效,同名变量赋值之后(用新的数据覆盖了该window对象属性原有的值),变量生效(但是要注意,在firefox 下, 在 with 伪闭包内声明的函数,只能在声明之后才能被调用,即,firefox 的 with 内没有对函数预先声明)。复制代码 代码如下:with; //在 firefox 下 a 是未声明function a(){ console.log("function a is called");} } 如果同名称的函数被多次声明,后面声明的将覆盖前面声明的,如:复制代码 代码如下:alert{alert{

复制代码 代码如下:var a = "outside";function f(){ var a = "inside"; function g(){return a;} return g;}var result = f();console.log;//inside

var o = {name: "qin", old: 23};

alert;

在这个例子中,当运行f函数被定义,同时创建了g闭包包含了g_invokeObj-f_invokeObj对象链,因此保存了f()函数执行过程中的变量a的值。当执行console.log()语句时,由于g函数对象仍然存在,因此g()闭包也依然存在;当运行这个仍然存在的g函数对象时,JavaScript会使用依然存在的g()闭包并从中获取变量a的值。

test(o);

}

当传入参数为object时,会执行if判断语句中的代码块,这个时候j的值打印出来为0;

alert{alert{ //这是最后一次声明的func1,以该函数为准

var o = "qin";

alert;

test(o);

}

当传入参数不为object的时候,就不会执行if判断语句中的代码块,这个时候j的值打印为undefined。这个时候的变量j就属于定义了未初始化。

alert{alert;}

JavaScript的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。而且JavaScript函数里声明的所有变量都会被提前至函数顶部,这叫做声明提前(hoisting)。

var func1 = function(){ //注意 ,这里是变量赋值,不是函数声明

在JavaScript中每一个全局代码或函数所包含的代码块中,都有一个与之关联的作用域链(scope chain),只有弄懂这个作用域链才能更好的去理解闭包问题。

alert;

作用域链是一个对象列表或者链表,当执行JavaScript代码需要查找变量y的值的时候(这个过程叫做“变量解析”),它会从链表中的第一个对象开始查找,如果这个对象中有一个名为y的属性,则会直接使用这个值,如果没有,就会继续查找下一个对象,以此类推。当整个链表的对象中都没有y这个属性的话,就会抛出一个引用异常(ReferenceError)的错误。

}

简单理解就是:子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

alert; //弹出function;} 除了 IE8 及IE8以下的浏览器,表达式中的函数声明都会返回匿名函数,不会成功声明具名函数复制代码 代码如下:if{ alert; // error,不会成功声明名称为 fun 的函数,但在IE8 及以下的浏览器中中会成功声明一个函数 fun

那么这个作用域链表对象创建规则是怎样的呢?大致分三种情况:

}

1.整个代码不包含任何函数:

;

这时的作用域链对象是由一个全局对象组成。比如:

alert; //error但是即使在 IE8 一下, 表达式中的具名函数也不能覆盖该作用于下同名的变量:

var n = "qin";

var fun = 1; //该变量不能被函数表达式中的函数名覆盖var f = function fun; //function fun; //1 注意区别:

if(typeof n == "string") {

if{ alert; // ok,这里声明了一个变量,该变量保存了一个匿名函数

for(var i = 0; i < 5; i++) {

}

console.log(i);

js函数是引用型的对象

}

var a = function(){};var b=a;b.x=2;alert; //2

}

二、函数的参数

console.log(i);

js函数不会检查函数调用时传入的参数个数与定义他时的形式参数个数是否一致,一般地,js函数调用时可以接收的参数个数为25个,当然不同的浏览器可能有差异,ECMAScript标准对这一点并没有规范。

上面这段代码中的作用域链为{n: "qin", i: 5}。

如果你不确定函数调用时传入了多少个参数,可以使用函数的arguments对象。

2.整个代码包含函数,但不包括嵌套函数:

arguments 有点像数组,arguments.length 为传入的参数个数,arguments[0] 是第一个参数,arguments[1]是第二个参数,类推...

这时的作用域链有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。比如:

函数对象的length属性:这个属性很少用到,甚至很少人知道,函数的length属性就是该函数定义时的形式参数个数。复制代码 代码如下:function myfun{

var x = "qin";

alert; //弹出调用时实际传入的参数个数

function test() {

alert; //对应参数a

var k = 3;

return a+b;

for(var i = 0; i < 3; i++) {

}

k += 1;

alert; //形参个数,2 arguments对象还有其他属性,比如常用的arguments.callee ,指向该函数自身。

}

要注意:如果函数内部声明了与形参同名的子函数,arguments 的相应值也会被修改,但是,在作用域内使用 var 声明了同名的 变量则不会导致 arguments 的参数值被函数替换。复制代码 代码如下:function aa{ //js 群的一道题 function a; //function a console.log; //如果作用域内没有 var a ,则 arguments[0] 为 function a 则一定是function a) console.log; var a = "ee"; //注销此句,考擦 arguments[0] 将变为 a 函数 var aa = "444"; arguments = 6; console.log; console.log; 三、函数的返回值

console.log(k);     // 打印结果为6

js函数使用 return 语句返回值。

}

一切数据类型都可以作为函数的返回值,js函数也可以没有返回值。

test();

四、函数调用

console.log(x);    // 打印结果为qin

函数自己是不会运行的,当它运行时,总是存在一个调用它的对象。

console.log(i);    // 会抛出ReferenceError错误

默认情况下,在任何语法环境中,如果没有显式指定函数的调用对象,就是指通过window对象来调用该函数,此时,函数体内的this指针指向window对象。复制代码 代码如下:function myfun{

上面这段代码中存在两个作用域链,第一个是函数局部变量的作用域链{i: 3, k: 6, x: "qin"},第二个是全局对象的作用域链{x: "qin"}。当我们在函数外部打印i的值的时候,JavaScript会去全局对象的作用域链查找属性为i的值,但是全局对象的作用域链并不存在i这个属性,因此就会抛出引用异常错误(ReferenceError)。

alert;

3.代码中存在函数,且有嵌套函数:

return a+b;

这时的作用域链至少有三个对象(因嵌套函数的数量增加而增加),第一个是全局对象的作用域链,第二个是最外层函数的参数和局部变量的作用域链,第三个是嵌套函数的参数和局部变量的作用域链。比如:

}

var x = 0;

myfun; // 调用函数并传入2个参数,这2个参数分别对应形式参数a,b调用函数时,如果传入的参数个数超过形式参数,就只有用arguments加下标来接收了。 由于没有显式指定调用函数的对象,alert将弹出 window对象。这种调用方法是最常见的。

function test() {

用于显式指定函数的调用对象方法有三个:

var y = 2;

1、如果一个函数被赋为一个对象的属性值,这个函数只能通过该对象来访问,通过该对象调用这个函数的方式类似以面向对象编程语言中的方法调用。复制代码 代码如下:var obj={}; //定义一个对象

x += 1;

obj.fun=function{

function foo() {

alert; //弹出this指针

var a = 3;

return a+b;

console.log(y);    // 结果为2

} //对象属性值为函数

}

alert;// 访问fun函数。 只能通过该对象来访问这个函数

foo();

obj.fun; //通过obj对象来调用fun函数,将弹出obj对象。这种方式也称为调用obj对象的fun方法。 2、 任意指定函数的调用对象:在某个语法环境中,如果可以同时访问到函数fun和对象obj,只要你愿意,可以指定通过obj对象来调用fun函数。指定方法 有2种:call方法和apply方法。(因为window对象是浏览器环境下的顶级对象,在任何语法环境中都能访问到window对象,因此,任何函数 都可以通过window对象来调用)复制代码 代码如下:function fun{

console.log(a);    // 会抛出ReferenceError错误

alert;

}

return a+b;

test();

}

console.log(x);    // 结果为1

var obj={};

上面这段代码中,函数test中包含一个嵌套函数foo,这段代码含三个作用域链,第一个是全局对象作用域链{x: 1},第二个是函数test的作用域链{y: 2, x: 1},第三个是嵌套函数foo的作用域链{a: 3,y: 2, x: 1}。

fun.call; //通过obj对象来调用fun函数,并传入2个参数,弹出的指针为obj对象。

当我们定义一个函数的时候,其实它就已经保存了一个作用域链。当我们调用这个函数,它会创建新的对象来存储它的局部变量,并将这个对象添加到保存的那个作用域链上,同时还会创建一个新的更长的表示函数调用作用域的“链”。对于嵌套函数来说,每次调用外部函数的时候,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都会有微妙的差别——在每次调用外部函数时,内函数的代码都是相同的,而且关联这段代码的作用域链也不相同。(引用自《JavaScript权威指南》)

var obj2={};

关于变量作用域及函数作用域可参考这篇文章:什么是变量作用域和函数作用域?(坑未填)

obj2.fun2 = function{ //obj2对象的属性fun2是一个函数

说完函数作用域和作用域链,接下来就要开始理解什么是闭包了。

alert;

首先在上面包含嵌套函数的例子中,我们如何在外层函数test中访问到嵌套函数foo中的变量a的值呢?

return a+b;

其实很简单,我们只需要把foo中a变量返回就可以了,如下:

};

var x = 0;

obj2.fun2.call; //通过obj对象来调用obj2对象的fun2属性值所保存的函数,弹出的this指针是obj对象

function test() {

//比较隐蔽的方法调用:数组调用一个函数[9,function; }][1]();

var y = 2;

//使用window对象调用函数下面几种方法是等价的fun; //如果fun函数是全局函数fun.call;fun.call; //如果该句代码在全局环境下,因为该语法环境下的this就是指向window对象。func.call(); //如果函数不需要传参func.call;func.call;var name = "window";function kkk(){console.log; // not ie}kkk(); //windowkkk.call; //kkk 函数被自己调用了 另一种比较容易疏忽的错误是,在A 对象的方法中,执行了使用了 B 对象的方法调用,试图在 B 对象的方法里使用 this 来访问 A 对象,这在各种回调函数中比较常见,最常见的情形就是 ajax 回调函数中使用 this 。复制代码 代码如下:var obj = { data:null, getData:function(){ $.post(url,{param:token},function{ //jQuery ajax post method this.data = dataBack; //试图将服务器返回的数据赋给 obj.data ,但这里的 this 已经指向 jQuery 的 ajax 对象了 },'json'); }}

x += 1;

//正确做法var obj = { data:null, getData:function(){ var host = this; //保存 obj 对象的引用 $.post(url,{param:"token"},function{ host.data = dataBack; },'json'); }} 3、apply方法调用:

function foo() {

apply方法与call方法唯一不同的地方是函数传参方式不同。

var a = 3;

obj2.fun2.call; 改为 apply方式就是obj2.fun2.apply;

console.log(y);    // 结果为2

apply使用类数组方式传参,除数组外,还可以使用arguments、HTMLCollection来传参,但arguments并非数组,如:

return a;

var obj={};

}

function fun_1{

var res = foo();

function fun_2{

console.log(res);// 结果为3

return a+b;

}

}

test();

fun_2.apply; //用fun_1的arguments对象来传参,实际上是接收了x,y

console.log(x);    // 结果为1

}apply 传参在IE8 及IE8一下的浏览器中哟2个问题

其实上面的代码就是典型的闭包,闭包函数为foo。

在 call 和 apply 调用中,如果传入标量数据(true/false ,string,number),函数运行时将把他们传入的基本数据包装成对象,然后把this指向包装后的对象,试试下面的代码。 function a(){

个人理解闭包的概念就是:

alert;

有权访问另一个函数作用域内变量的函数就是闭包。

alert;

本质上闭包就是将函数内部与外部联系起来的一座桥梁。

alert;

闭包的用途:

}

闭包的最大用处有两个:

a.call;

第一是上面所说的可以读取到函数内部的变量。

a.call;

第二则是让这些变量值能一直保存在内存中。

a.call;

来看一个比较有趣的例子:

甚至可以用这个特点来传参数,但是不建议这种用法:

function makeAdder(x) {

function a; } //对象在运算中自动进行类型转换

return function(y) {

a.call; //101

return x + y;

4、函数作为对象构造器

版权声明:本文由龙竞技官网发布于竞技宝竞猜,转载请注明出处:javascript 函数及作用域总结介绍_javascript技巧_脚本之家