JS与ES6高级编程学习笔记(二)——函数与作用域
一、概述
开发者常戏称"函数是JavaScript中的一等公民",这足以体现了函数的重要性,为了更好的掌握函数我们需要学习函数的构造器Function等相关内容。
因为JavaScript的作用域与我们学习过的静态语言(如Java、C#等)有非常大的区别,理解作用域对更加深入的掌握JavaScript是非常有帮助的。
二、Function与函数
JavaScript中的函数(function)有着举足轻重的作用,函数是对象,每个函数都是Function类型的实例。要深入理解JavaScript就必须了解Function。
2.1、构造函数
构造函数可以用来创建对象。构造函数与普通的函数类似,一般首字母大写。使和new操作符调用构造函数可以创建一个新对象并自动返回,返回的对象类型就是该构造函数类型。我们可以通过构造函数的原型中的constructor属性访问对象的构造器。每一个对象都有__proto__属性,指向其的构造函数的prototype属性对象,用来实现继承关系。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> //定义构造函数,类 function Student(name,age){ this.name=name; this.age=age; this.show=function(){ console.log("大家好,我是"+this.name+",今年"+this.age+"岁!"); } } //实例化对象 var tom=new Student("tom",18); //调用对象中的方法 tom.show(); //tom就是Student的一个实例 console.log(tom instanceof Student); //每一个对象都有一个__proto__的属性,指向构造器的原型对象 console.log(tom.__proto__); console.log(tom.constructor); </script> </body> </html>
运行时的状态如图2-1所示。
图2-1 构造函数示例输出结果
构造函数也是一个可以直接调用的函数,因为没有显式的返回值所以结果为undefined,但在严格模式("use strict")下直接调用构造函数会提示错误;构造函数默认返回this,可以覆盖。
2.2、Function对象创建
每一个function(函数)都是Function的实例,函数是对象,函数名是指针,创建function主要有3种不同的方式:
(1)、函数声明语法定义
/**声明函数add_1*/
function add_1(m,n) {
return m+n;
}
(2)、函数表达式
/**使用函数表达式定义函数add_2*/
var add_2=function (m,n) {
return m+n;
}
(3)、构造函数定义
使用Function构造函数可以接受任意数量的参数,但最后一个参数是函数体,语法格式如下:
new Function ([arg1[, arg2[, ...argN]],] functionBody)
一般形式:
var functionName=new Function("参数","参数","…","函数体");
使用Function构造函数定义一个加法函数如下:
/**使用Function构造函数定义函数add_3*/
var add_3=new Function("m","n","return m+n;");
测试代码:
console.log(add_1(1,1));
console.log(add_2(2,2));
console.log(add_3(3,3));
/**add_4指向add_3的引用*/
var add_4=add_3;
console.log(add_4(4,4));
/**获取函数的构造器*/
console.log(add_1.constructor===Function);
console.log(add_1.constructor===add_2.constructor);
console.log(add_2.constructor===add_3.constructor);
运行时的状态如图2-2所示。
图2-2 函数定义示例输出结果
从输出结果可以知道三种定义函数的方式的构造器都是Function;第三种定义的函数add_3是一个函数表达式,这种方式不推荐,因为将一个很长的函数定义在字符串中会影响语法检查,而且会降低性能,因为使用Function构造器生成的Function对象是在函数创建时解析的;另外函数名是一个指向函数的指针,可以认为它就是一个变量。
函数声明与函数表达式的区别是解析器对这两种定义函数方法的解析是不一样的。解析器会将函数声明的函数优先解析,使其在代码执行前可用(函数声明提前)。而函数表达式会在执行到该行代码才会被解析。
2.3、属性
Function是一个函数也是一个对象,Function对象并没有自己的属性和方法,它也会通过原型链从自己的原型链Function.prototype上继承一些属性和方法,这些属性与方法可以在每一个定义的函数中使用,Function实例从Function.prototype继承这些属性和方法。
使用console.dir(Function)打印的结果如下:
(1)、Function.arguments[] (不推荐)
以数组形式获取传入函数的所有参数。不推荐使用该属性,已被arguments 替代。ES6中可以使用rest参数,单独使用时返回null。
注意:Function.arguments[]与内部属性arguments是不一样的。
(2)、length
在声明函数时指定的命名参数的个数。
(3)、name (非标准)
获取函数的名称。
(4)、caller (非标准)
对调用当前函数的Function对象的引用,如果当前函数由顶层代码调用,这个属性的值为null。可以简单的理解为就是调用当前函数的函数。
(5)、prototype
原型对象,用于构造函数,这个对象定义的属性和方法由构造函数创建的所有对象共享。
/**声明函数add*/
function add(m,n) {
console.log("调用函数:"+add.caller);
return m+n;
}
console.log("参数个数:"+add.length);
console.log("函数名称:"+add.name);
console.log("参数数组:"+add.arguments);
console.log("原型对象:"+add.prototype);
console.log("调用函数:"+add.caller);
/**直接调用add函数*/
add(1,1);
/**通过sum函数间接调用add*/
function sum() {
add(2,2);
}
sum();
运行时的状态如图2-3所示。
图2-3 函数属性示例输出结果
在输出结果中可以看到调用的函数对象第一个为null,因为add函数并没被调用,第二个为null是因为add是被直接调用的,第三个输出的是sum的toString()值,因为add是被sum间接调用的,这里可以直接用add.caller()调用sum函数,也就是说下列的代码应该输出true:
console.log(add.caller===sum);
2.4、方法
(1)、apply( )
在指定的一个对象上下文中调用另一个对象的方法,将函数作为指定对象的方法来调用,传递给它的是指定的参数数组。参数说明如下:
apply([thisObj[,argArray]])
thisObj用于指定当前调用的上下文,用于替代当前对象。
argArray是参数数组,如果 argArray不是一个有效的数组或者不是arguments对象,那么将导致一个TypeError。
如果没有提供 argArray 和 thisObj 任何一个参数,那么Global对象将被用作thisObj,并且无法被传递任何参数。
/*定义构造函数*/
function Student(name, age) {
this.name = name;
this.age = age;
}
/**定义函数表达式show*/
var show = function (greeting, height) {
console.log(greeting + ":" + this.name + "," + this.age + "," + height);
}
//通过new关键字调用构造函数,创建对象
var rose = new Student("rose", 18);
var jack = new Student("jack", 20);
//调用show函数,指定上下文,指定调用对象,this指向rose,数组是调用show时对应的两个参数
show.apply(rose, ["大家好", "178cm"]);
show.apply(jack, ["Hello", "188cm"]);
运行时的状态如图2-4所示。
图2-4 apply函数示例输出结果
同一个方法调用时动态的指定了this对象所以输出了不同的结果,show方法默认没有指定this,此时的this指向window对象。
<script> var result=Array.prototype.sort.apply([3,2,1]); console.log(result); </script>
结果:
(2)、call( )
功能与apply类似,将函数作为指定对象的方法来调用,传递给它的是指定的参数。
对于第一个参数意义都一样,但对第二个参数:
apply传入的是一个参数数组,也就是将多个参数组合成为一个数组传入,而call则作为call的参数传入(从第二个参数开始)。
如func.call(func1,var1,var2,var3)对应的apply写法为:func.apply(func1,[var1,var2,var3])
同时使用apply的好处是可以直接将当前函数的arguments对象作为apply的第二个参数传入
//在window对象下定义name与age属性
var name="mark";
var age=88;
//直接调用show方法未指定this时this指向window
show("Hi","187CM");
//调用show函数,指定上下文,指定调用对象,this指向rose
show.call(rose, "大家好", "178cm");
//Hello与188是参数,注意这里不使用数组
show.call(jack, "Hello", "188cm");
运行时的状态如图2-5所示。
图2-5 call函数示例输出结果
从运行结果可以看出直接调用show方法时this指向了window对象,直接定义的name与age属于window对象下,可以使用window.name访问到。call与apply的明显区别是参数传递不一样,apply使用数组而call直接传递参数。
(3)、bind()
bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入bind()方法的第一个参数作为this,传入bind()方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。
语法:
function.bind(thisArg[,arg1[,arg2[, ...]]])
参数:
thisArg
调用绑定函数时作为this参数传递给目标函数的值。如果使用new运算符构造绑定函数,则忽略该值。当使用bind在setTimeout中创建一个函数(作为回调提供)时,作为thisArg传递的任何原始值都将转换为object。如果bind函数的参数列表为空,执行作用域的this将被视为新函数的thisArg。
arg1, arg2, ...
当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。
返回值:
返回一个原函数的拷贝,并拥有指定的this值和初始参数。
//将show函数绑定上下文与参数
//调用bind返回绑定好上下文件的函数
var fun=show.bind(rose, "大家好", "178cm");
console.log(fun);
//调用绑定后的函数
fun();
运行时的状态如图2-6所示。
图2-6 bind函数示例输出结果
小结:apply、call、bind都可以动态的指定被调用函数的上下文,apply调用的方法参数以数组的形式调用,call与bind则直接指定,bind返回绑定好的方法并不会执行而call与apply会立即执行。
(4)、toString( )
返回函数的源码字符串形式。
(5)、toSource() (非标准)
返回函数的源码字符串形式。
(6)、isGenerator() (非标准)
函数对象是否为generator。
2.5、内部属性
JavaScript中每个函数内部都有两个非常特殊的对象:this与arguments。
(1)、this
this与Java和C#中的this相似,函数内部属性this一般指向的是函数执行时的上下文对象,没有时指向window对象。
//定义学生对象
var student = {
name: "rose", //姓名属性
show: function () {
console.log(this.name||"匿名"); //访问当前对象的name属性
}
};
student.show();
//获得方法的引用
var showRef=student.show;
//间接调用show方法,此时this指向window对象
showRef();
var name="jack";
showRef();
运行时的状态如图2-7所示。
图2-7 this对象示例输出结果
从输出结果可以得知this第一次指向了student对象,第二次指向了window对象,name值为空,逻辑运算后返回了"匿名",第三次指向了window对象,但此时定义了name属性所以输出该值。
this指向的对象需要根据不同的执行环境来判断,并不是固定不变的,总结如下:
1)、this默认指向当前对象,没有时指向window对象;
2)、通过call、apply、bind调用函数时可以指定this对象;
3)、构造函数中的this指向被创建的对象,如var tom= new Student(),Student中的this指向tom对象;
4)、通过addEventListener注册的事件中的this指向绑定事件的元素,也就是event.currentTarget,注意event.target返回触发事件的元素;
5)、内联事件中的this指向触发事件的元素DOM元素,如<input onclick='login_click(this)' />this指向当前input元素;
6)、内联事件函数中的this指向window对象,如function login_click()中使用this;
<input type="button" value="登录" onclick="login_click(this)" />
<div id="divBox">
<input type="button" value="注册" id="btnSignup"/>
</div>
<script>
function login_click(inputButton) {
console.log(inputButton);
console.log(this);
}
document.getElementById("divBox").addEventListener("click",function (e) {
console.log(this);
console.log(e.currentTarget);
console.log(e.target);
console.log(this==e.currentTarget);
});
</script>
运行时的状态如图2-8所示。
图2-8 this对象示例输出结果
(2)、arguments
正在执行的函数的内置属性,arguments是一个对应于传递给函数的参数的类数组对象。主要解决在函数调用的时候,在不设置形参的情况下使用,函数未调用时并不能获得该对象。
length属性:返回实际传入的参数个数。
callee属性:返回当前函数的引用(匿名函数可以使用该属性实现递归调用)。
function sum() {
console.log("参数个数:"+arguments.length);
//遍历所有参数
for(var i=0;i<arguments.length;i++){
console.log(arguments[i]);
}
console.log(arguments.callee==sum);
}
sum(1,2,3);
sum('a','b');
运行时的状态如图2-9所示。
图2-9 arguments对象示例输出结果
arguments对象具有length属性和索引访问方式,看起来与数组非常相似,但arguments并不是数组,它没有数组对象所具备的其他成员属性和方法。
三、作用域
JavaScript没有块级作用域、没有类、没有包、也没有模块,这有有别于常见的编程语言,如C、Java、C#等,经常会导致理解上的困惑,如果没有理解JavaScript中的作用域就不能很好的理解JavaScript程序。
3.1、作用域与块
(1)、作用域
作用域(scope)就是程序中成员的可访问范围,即作用域控制着这些成员的可见性和生命周期。不同的编程语言可能有不同的作用域和名字解析。而同一语言内也可能存在多种作用域。作用域又可以分为静态作用域与动态作用域。
静态作用域又叫做词法作用域,词法变量有一个在编译时静态确定的作用域。大多数现在程序设计语言都是采用静态作用域规则,如C/C++、C#、Python、Java、JavaScript等。
动态作用域的变量叫做动态变量。只要程序正在执行定义了动态变量的代码段,那么在这段时间内,该变量一直存在;代码段执行结束,该变量便消失。
(2)、全局作用域与局部作用域
JavaScript中有全局作用域(Global Scope)与局部作用域(Local Scope)两种作用域类型。
不在函数内定义的成员具有全局作用域。JavaScript默认有一个全局对象window,全局作用域的变量实际上被绑定到window对象。window对象的内置属性默认拥有全局作用域。
局部作用域(Local Scope)。在一个函数中定义的变量只对这个函数内部可见,称为局部作用域,也称为函数作用域。
(3)、块
用大括号"{}"将多行代码囊括起来,并形成一个独立的代码区间的代码形式称为代码块。
下面是一段Java代码,通过这段代码来理解一下作用域与块的概念。
/**包*/
package com.project.math;
/**类,块*/
public class Rectangle {
/**成员变量,作用域在整个类中有效*/
public int width;
public int height;
/**方法,块*/
public int getArea(){
/**局部变量,作用域只在当前方法中有效*/
int area=0;
area=this.width*this.height;
return area;
}
public static void main(String[] args) { //代码块
//变量i的作用域只在当前循环中有效
for (int i=1;i<=10;i++){ //代码块
System.out.println(i);
if(i%2==0){ //代码块
System.out.println(i);
//变量j只在if这个块中有效
int j=i;
}
//System.out.println(j); //越级访问,j不可见
}
//System.out.println(i); //i不可见,访问不了
//代码块
{
int k=100; //k的作用域仅在当前块中有效
}
//System.out.println(k); //k不可见,访问不了
}
}
从上面的代码不难看出块在Java中界定了成员的访问范围,定义在块中的成员可用的作用域仅在当前块中,越级是访问不了的。
3.2、JavaScript没有块级作用域
与其它的高级语言不同,JavaScript没有块级作用域,下面这段代码有你意想不到的结果:
<script>
if(true){
var i=100;
}
console.log(i);
</script>
运行时的状态如图2-10所示。
图2-10 JavaScript没有块级作用域示例输出结果
上述代码在if中定义了变量i,因为没有块级作用域,所有在作用域外再访问i依然是可见的,但类似的代码在Java或C/C++中是不一样的,i会在if语句执行完成后被销毁,但JavaScript中会将i添加到当前执行环境中(这里是全局环境)。
public class ScopeTest { //Java代码
public static void main(String[] args) {
if(true){
int i=100;
}
System.out.println(i); //错误,i不可见
}
}
因为JavaScript中有块级作用域的限制,在if中定义的i在该作用域外是访问不到的,所以会提示编译错误。
提示:在ES6中新增加了变量声明的关键字let与const,用let和const声明的变量可以受到块作用域的约束,只能在定义它们的块中访问。
3.3、函数作用域
虽然JavaScript没有块级作用域但它拥有函数作用域,函数作用域意味着在函数中定义的成员在函数内部是可见的,但是在函数外部不可见。
function foo() {
var i=100;
console.log("函数内部访问:i="+i);
}
foo();
console.log("函数外部访问:i="+i);
运行时的状态如图2-11所示。
图2-11 函数作用域示例输出结果
因为受到函数级作用域的限制,i的可见范围只在foo函数中,在外部访问不了,所以运行时提示i没有定义的错误。
3.4、作为值的函数
因为函数名只是一个指向函数声明的指针,所以函数名也可以像变量一样使用,类似C++中的函数指针与C#中的委托。程序中可以将函数作为参数传递给另一个函数,也可以将函数作为返回值。
(1)、函数作为参数
//声明加法函数
function add(num1,num2) {
return num1+num2;
}
//fun接收函数的引用(函数变量),m,n是参数
function handler(fun,m,n) {
//调用函数并输出结果
console.log("结果是:"+fun(m,n));
}
//将函数add作为参数传递给handler函数
handler(add,100,200);
//匿名函数表达式作为参数
handler(function (m,n) {
return m-n;
},300,200);
运行时的状态如图2-12所示。
图2-12 函数作为参数示例输出结果
第1次调用handler将add函数传递给了handler函数的fun,在handler中调用fun时其实是间接的调用了add函数;第2次调用handler将一个匿名函数表达式传递给了fun参数,调用时可以理解为就是调用了这个匿名的函数表达式。
函数作为参数的应用非常广泛,使用较多的就是回调函数与事件绑定。
(2)、函数作为返回值
//返回函数的函数
function mathFactory(type) {
return function (num1,num2) {
return type==='+'?num1+num2:num1-num2;
}
}
//调用mathFactory函数获得函数的引用
var add=mathFactory("+");
//调用add函数
console.log(add(500,300));
//调用mathFactory返回的函数
console.log(mathFactory("-")(500,200));
运行时的状态如图2-13所示。
图2-13 函数作为返回值示例输出结果
接收函数作为参数的函数,都可以叫做高阶函数。我们常常利用高阶函数来封装一些公共的逻辑。
柯里化(Curry)就是高阶函数的一种特殊用法。柯里化是指一个函数,他接收函数A作为参数,运行后能够返回一个新的函数。并且这个新的函数能够处理函数A的剩余参数。是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
3.5、全局变量
使用var声明的变量会自动添加到最近的上下文(环境)中。如果没有使用var声明,该变量会自动添加到全局上下文(环境)中。
在全局作用域中定义的变量是全局变量,没有使用关键字声明的变量是全局变量,全局变量有全局作用域,它可在整个程序中访问。
//定义全局变量i
var i=100;
function foo() {
console.log("i="+i); //访问全局变量i
j=101; //未使用关键字声明的局部变量j
var k=102; //使用关键字var声明的局部变量k
}
foo();
console.log("j="+j); //访问全局变量j
console.log("k="+k); //访问局部变量k
运行时的状态如图2-14所示。
图2-14 函数作用域示例输出结果
示例中i定义在全局作用域中,所有i是全局变量,在函数中可以访问到;j没有使用关键字声明,被自动添加到全局作用域中,j变成了全局变量。而k使用var声明,k的作用域是当前函数,k是局部变量,受函数作用域约束,所以在外部访问时提示k未定义的错误。
注意:开发中需特别小心对外暴露全局变量,因为很容易引起冲突,比如你引用了一个第三方的脚本,脚本中暴露了一个名为i的全局变量,恰好你也定义了一个相同的全局变量,并修改了该变量的值,那么第三方脚本中的全局变量就被污染了。
3.6、IIFE(立即执行函数表达式)
默认情况下JavaScript并没有块级作用域,只有函数级作用域,这样会产生许多问题,如全局成员污染、可读性差、不便扩展与复用等。必要时我们可以通过函数表达式与立即执行函数表达式创造一个私有的块级作用域来模拟块级作用域的约束。
Immediately-Invoked Function Expression(简称IIFE)即立即执行函数表达式,是一个在定义时就会立即执行的JavaScript匿名函数,受函数作用域的约束IIFE不仅避免了外界访问此IIFE中的变量,而且又不会污染全局作用域。用作块级作用域的匿名函数的一般语法如下所示:
(function () {
//这里是块级作用域
})();
第一个圆括号中放了一个匿名函数,通过括号运算将匿名函数转换成了函数表达式,匿名函数不允许直接调用,因为以function关键字开始会当作函数声明,函数声明后面不允许使用圆括号,而函数表达式可以直接调用,第二个圆括号表示立即调用该函数,这就是立即执行函数表达式。因为JavaScript中有函数级作用域的原因所以这块区块是私有的。
(function () {
//这里是块级作用域
var i=100;
console.log("i="+i); //输出i=100
})();
console.log(i); //提示错误 Uncaught ReferenceError: i is not defined
(1)、IIFE的多种形式
看来只需要将一个匿名函数转换成函数表达式就可以立即调用了,将匿名函数声明转换成函数表达式的方法有很多,根据这个要求我们可以使用多种方式来写IIFE。
//最常见的2种写法
(function(){ /* code */ }());
(function(){ /* code */ })();
//通过操作符区分函数表达式和函数声明
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
// 一元运算符
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();
// 这样也可以
new function(){ /* code */ };
new function(){ /* code */ }();
如果是函数表达式,可直接在其后加"()"立即执行。如果是函数声明,可以通过"()"、"+"、"-"、"void"、"new"等运算符将其转换为函数表达式,然后再加"()"立即执行。这些声明都是正确的但性能上还有有些区别。
(2)、参数
函数表达式也是函数的一种表达形式,同样可以像函数一样使用参数,如下所示:
(function(n) {
console.log(n);
})(100);
输出:100
IIFE不仅可以形成块级作用域而且可以提高性能,因为当块内的程序在使用外部对象时将优先查找块内的对象,再查找块外的对象,依次向上。
(function(root){
//查找root的范围缩小到当前函数中
root.console.log("Hello");
})(typeof window !== 'undefined' ? window : this);
输出:Hello
(3)、添加分号
为了避免与其它的JavaScript代码产生影响后报错,常常会在IIFE前增加一个分号,表示前面所有的语句都结束了,开始新的一语句。因为JavaScript并不强制要求语句以分号结束。
var m = 200
(function (n) {
console.log(n);
})(m);
上面的脚本会报错,解释器会认为200是函数名。
;(function (n) {
console.log(n);
})(m);
在第一个圆括号前增加了一个分号这样就正确了。当自定义插件时会使用IIFE,这是一段独立的代码,在应用过程中不能保证用户会加上分号,所以建议在IIFE前加上分号。
(3)、IIFE的变形
如下所示当IIFE中的代码行数较多时想要看到参数就要去查找了,非常不方便。
;(function (n) {
console.log(n);
//假定这里有30000代码
}(300));
修改后的代码中有两个函数表达式,一个作为参数,就是我们主要要完成的功能向控制台输出数字,另一个作来IIFE立即执行的函数,主要的功能函数变成的IIFE的参数了,它是一个匿名函数,变形后如下所示:
;(function (number,factory) {
factory(number);
}(300,function(n){
console.log(n);
//假定这里有30000代码
}));
调用时将参数300赋值给了number,将第2个匿名函数赋值给了factory,在IIFE中调用factory,将参数number的值再传递给调用factory时的参数n,最后输出300。
(4)、IIFE的优点
提高性能,减少作用域查找时间。JavaScript解释器首先在作用域内查找属性,然后一直沿着链向上查找,直到全局范围。
;(function(window, document,$){
})(window, document, window.jQuery);
压缩空间。通过参数传递全局对象,压缩时可以将这些全局对象更名为一个更简单的名称。
;(function(w,d,$) {
//这里直接使用w,d与$代替window,document与jQuery
})(window,document,window.jQuery);
避免冲突。匿名函数内部可以形成一个块级的私有作用域。
四、闭包
闭包(Closure)是许多语言中都拥有的一种特性,JavaScript支持闭包,理解闭包对实现复杂应用与理解JavaScript都有非常大的帮助,理解了其解释和运行机制才能写出更为安全和优雅的代码。
4.1、闭包的概要
闭包就是能够读取其他函数内部变量的函数,定义在一个函数内部的函数,闭包是将函数内部和函数外部连接起来的桥梁。JavaScript拥有闭包特性的原因是"链式作用域(chain scope)"结构,子对象会一级一级地向上寻找所有父对象的变量,内部函数会查找外面函数的成员,当内部函数被外部引用时其访问的外部函数成员依然会驻留在内存中。
闭包的作用。因为局部变量无法共享和长期保存,而全局变量可能造成污染,所以我们希望有一种机制既可以长久的保存变量又不会造成全局污染。
闭包的特点。占用更多内存;不容易被释放,容易引起内存泄漏。
一般的用法如下:
(1)、定义外层函数,封装被保护的局部变量,因为函数级作用域。
(2)、定义内层函数,对外部函数变量的访问。
(3)、外层函数返回内层函数的对象,并且外层函数被调用,结果保存在一个全局的变量中。
//声明外层函数
function outer() {
var i=0;
function inner() { //声明内层函数
console.log(++i); //内层函数访问外存函数的变量i
}
return inner;
}
//调用外层函数获得内层函数的引用
var fun=outer();
//调用内层函数
fun();
fun();
fun();
运行时的状态如图2-15所示。
图2-15 闭包定义示例输出结果
上面就是一个典型的JavaScript闭包示例,在外部通过调用outer函数获得内层函数的引用,再调用内层函数时i并没有被释放,一直驻留在内存中,此时i是受保护的,外部不能直接访问。
4.2、闭包的使用
前面我们理解了作为一个函数变量的一个引用当函数返回时,其处于激活状态。一个闭包就是当一个函数返回时,一个没有释放资源的内存栈。当我们想将一个变量驻留在内存中又不想引起全局污染时就可以使用闭包。
(1)、获得一个唯一编号
比如我们现在想通过一个函数返回一个唯一的编号,在整个页面的生命周期内都有效,可以使用闭包。
//通过IIFE返回一个内部函数
var getId=(function () {
var id=10000000;
return function () {
return ++id; //内部函数访问外部变量形成闭包
}
})();
console.log(getId()); //10000000
console.log(getId()); //10000001
console.log(getId()); //10000002
运行结果是:10000000、10000001、10000002
思考:如果将IIFE的第一对圆括号去除(var getId=function)代码可以正常运行吗?为什么?
(2)、封装
通过结合前面学习过的IIFE、作用域与闭包实现属性封装。
var student=(function () {
//作用域仅在当前函数中的变量age
var age=0;
return {
getAge:function () {
return age;
},
setAge:function (value) {
if(!isNaN(value)&&value>=0&&value<=150){
age=value;
}else{
throw "年龄只能是介于0-150之间的数字";
}
}
}
})();
console.log(student.age); //直接访问
student.setAge(18); //写
console.log(student.getAge()); //读
student.setAge(-1); //非法值
运行时的状态如图2-16所示。
图2-16 封装示例输出结果
直接访问输出undefined是因为age的作用域为当前函数,抛出异常的原因是设置的年龄值不合要求。
(3)、缓存
开发中我们常常需要完成查找等一些耗时的功能,利用闭包可以将要查找的数据缓存起来,这样可以提高程序的性能。查找前先判断数据是否在缓存中,如果在缓存中则直接返回,如果不在则通过耗时的操作获取。
var searchUser=(function () {
var cached={}; //用于存放缓存数据的对象
return {
byId:function (id) { //通过编号获得用户
if(id in cached){ //如果用户在缓存中
return cached[id]; //直接返回编号对应的用户对象
}else{
//模拟从远程获取对象
cached[id]={name:Math.random(),id:id};
return cached[id];
}
}
}
})();
//通过编号查找编号为101的用户
console.log(searchUser.byId(101));
console.log(searchUser.byId(102));
console.log(searchUser.byId(101));
运行时的状态如图2-17所示。
图2-17 缓存输出结果
第一次查找编号为101的用户里因为缓存中并没有该对象所以需要去获取(这里只是模拟生成的,实际开发中可能需要使用AJAX从服务器加载),这个操作往往较耗时,第二次查找编号为101的用户时因为缓存中有数据,所以直接就获取到了。
闭包的应用远远不止这里提到的几个,有回调函数的地方多数都应用了闭包,关键是掌握闭包的特性。
4.3、注意事项
(1)不要滥用闭包。因为闭包会使得函数中的变量驻留在内存中,将消耗更多的内存,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
(2)、不要随便改变父函数内部变量的值。闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这样容易引起混乱。
五、Hoisting △
变量与函数提升(Hoisting)是JavaScript中执行上下文(特别是创建和执行阶段)一种特殊的工作方式,掌握Hoisting特性有助于理解JavaScript的编译特性。
5.1、变量提升
先来看一个示例:
<script>
console.log(a);
console.log(b);
var a=100;
b=200;
</script>
运行时的状态如图2-18所示。
图2-18 变量提升示例输出结果
按理来说a也应该直接提示异常,但并没有,原因分析如下:
JavaScript中var声明的变量具有hoisting特性,JavaScript引擎在执行的时候,会把所有变量的声明都提升到当前作用域的最前面。
上述的代码经过编译后的结果是:
//var关键字声明的变量会hosting,并且会赋初值为undefined
var a=undefined;
console.log(a);
console.log(b);
a=100;
//未使用var声明的变量,没有hosting
b=200;
在ES6中通过let和const关键字声明的变量也会提升,但不会被初始化,且在初始化之前是不能访问,被称之为暂时性死区,当试图在声明之前访问时会提示:Uncaught ReferenceError: Cannot access 'a' before initialization,即不能在初始化之前访问。如果a使用let或const声明变量也会被提升,"var a"这一段不会会赋值为undefined。
5.2、函数提升
先来看一个示例:
<script>
fun1();
console.log(fun2);
fun2();
function fun1(){
console.log("fun1");
}
var fun2=function(){
console.log("fun2");
}
</script>
运行时的状态如图2-19所示。
图2-19 函数提升示例输出结果
按理来说fun1在没有声明前调用应该提示错误,但这里并没有,而且调用了fun1并获得了输出结果,fun2没定义前也输出了undefined,原因分析如下:
声明式函数具有hoisting特性,会提升,var定义的函数表达式与变量一样会提升且赋值为undefined。
上述的代码经过编译后的结果是:
<script>
//声明式函数会提升
function fun1(){
console.log("fun1");
}
//var声明的变量会提升,且会赋初始为undefined
var fun2=undefined;
fun1(); //调用函数fun1,输出fun1
console.log(fun2); //输出fun2为undefined
fun2(); //因为fun2为undefined,作为函数调用报错
fun2=function(){
console.log("fun2");
}
</script>
函数和变量相比,会被优先提升。这意味着函数会被提升到更靠前的位置。
六、上机任务
6.1、上机任务一(40分钟内完成)
上机目的
1、理解Function与函数之间的关系
2、掌握函数内置属性的使用
上机要求
1、使用三种不同的方式声明一个阶乘函数,即n!=1×2×3×...×(n-1)n,当输入参数n返回阶乘结果。
2、使用递归完成一个阶乘函数,即0!=1,n!=(n-1)!×n,函数中不能出现同名阶乘函数,考虑间接引用。
3、JavaScript中的函数并没有重载特性,请写一个sum函数完成重载功能,要求如下:
sum(n):1参数时返回n++;如sum(1),返回2。
sum(n1,n2,n3…nx):返回n1+n2+n2+…nx;如sum(1,3,5)返回9,sum(1,2)返回3。
sum([n1,n2,n3…nx]):将数组中的数字累加,如果不是数字则跳过。如sum([1,'a',8])返回9。
使用IIFE封装代码并保存到一个独立的.js文件中,测试不同参数的输出结果。
推荐实现步骤
步骤1:编写JavaScript脚本,分阶段完成功能。
步骤2:测试运行效果,优化代码,关键位置书写注释,必要位置进行异常处理。
6.2、上机任务二(40分钟内完成)
上机目的
理解闭包并学习使用IIFE
上机要求
请在页面中放10个div,每个div中放入对应字母a-j,当点击每一个div时显示索引号,如第1个div显示0,第10个显示9。运行时的效果如图2-20所示。
图2-20 上机任务二要求效果
当点击f时输出的结果,请使用闭包与非闭包的两种方式实现该功能。
推荐实现步骤
步骤1:创建一个HTML文件,完成页面布局。
步骤2:编写JavaScript脚本逐步实现功能,先查找到所有的div,循环绑定事件。
步骤3:测试运行效果,优化代码,关键位置书写注释,必要位置进行异常处理。
6.3、上机任务三(90分钟内完成)
上机目的
掌握函数作为参数与返回值的应用,巩固DOM的使用。
上机要求
(1)、定义一个函数sortFactory,该函数接收一个任意参数属性名propertyName返回一个根据该属性排序的函数,从而达到根据指定属性名生成不同的排序函数的功能,调用该函数的示例如下所示:
var data=[
{name:"rose",age:19,id:"005"},
{name:"mark",age:16,id:"003"},
{name:"jack",age:18,id:"002"}];
data=data.sort(sortFactory("name")); //根据name排序
console.log(JSON.stringify(data));
data.sort(sortFactory("age")); //根据age排序
console.log(JSON.stringify(data));
data.sort(sortFactory("id")); //根据id排序
console.log(JSON.stringify(data));
输出结果如图2-21所示。
图2-21 排序后输出结果
(2)、使用DOM展示表格,添加排序事件,点击表头时完成排序功能,能实现升序与降序间的切换功能,完成时的效果如图2-22所示:
图2-22 排序运行效果图
推荐实现步骤
步骤1:创建一个HTML文件,先根据提示了解sort排序方法。
步骤2:创建sortFactory函数,根据propertyName返回一个匿名函数。
步骤3:完成排序功能,测试控制台排序效果。
步骤4:完成页面布局,编写表格排序功能的JavaScript脚本。
步骤5:测试运行效果,优化代码,关键位置书写注释,必要位置进行异常处理。
Array对象的sort方法提示:
语法:
arrayObject.sort(sortby)
sortby规定排序规则的函数。
返回对数组的引用。请注意,数组在原数组上进行排序,不生成副本。
说明:
如果调用该方法时没有使用参数,将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。要实现这一点,首先应把数组的元素都转换成字符串(如有必要),以便进行比较。
如果想按照其他标准进行排序,就需要提供比较函数,该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,返回值要求如下:
若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。
若 a 等于 b,则返回 0。
若 a 大于 b,则返回一个大于 0 的值。
6.4、代码题
声明一个Student构造方法,实现get+属性名与set+属性名的通用函数,能实现任意属性的访问功能,注意set函数要求实现级联效果,要求达到的效果如下代码所示:
function Student( properties ) {
//请完善这里的代码
}
var student = new Student({
name: "Candy",
age: 31
});
console.log( student.getname()); //Candy
console.log( student.getage()); //31
student.setname("Rose");
console.log(student.getname()); //Rose
student.setage(18);
console.log( student.getage()); //18
var stu = new Student({uid: "101", sex:"男"});
console.log(stu.getuid()+","+stu.getsex()); //101,男
stu.setuid(102).setsex("女");
console.log(stu.getuid()+","+stu.getsex()); //102,女
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构