详解JavaScript的闭包、IIFE、apply、函数与对象

目录

一、闭包(Closure)

1.1、闭包相关的问题

1.2、理解闭包

二、对象

2.1、对象常量(字面量)

2.2、取值

2.3、枚举(遍历)

2.4、更新与添加

2.5、对象的原型

2.6、删除

2.7、封装

三、函数

3.1、参数对象 (arguments)

3.2、构造函数

3.3、函数调用

3.3.1、call

3.3.2、apply

3.3.3、caller

3.3.4、Callee

3.5、立即执行函数表达式 (IIFE)

3.5.1、匿名函数与匿名对象

3.5.2、函数与函数表达式

3.5.3、立即执行函数表达式与匿名对象

3.5.4、各种IIFE的写法

3.5.5、参数

3.5.6、添加分号

3.5.7、IIFE的作用

3.5.8、IIFE的变形

四、示例下载

一、闭包(Closure)

1.1、闭包相关的问题

请在页面中放10个div,每个div中放入字母a-j,当点击每一个div时显示索引号,如第1个div显示0,第10个显示9;方法:找到所有的div,for循环绑定事件。

示例代码:

<!DOCTYPE html>
<html>
 <head>
 <meta charset="UTF-8">
 <title>闭包</title>
 <style type="text/css">
 div {
 width: 100px;
 height: 100px;
 background: lightgreen;
 float: left;
 margin: 20px;
 font: 30px/100px "microsoft yahei";
 text-align: center;
 }
 </style>
 </head>
 <body>
 <div>a</div>
 <div>b</div>
 <div>c</div>
 <div>d</div>
 <div>e</div>
 <div>f</div>
 <div>g</div>
 <div>h</div>
 <div>i</div>
 <div>j</div>
 <script type="text/javascript">
 var divs=document.getElementsByTagName("div");
 for (var i=0;i<divs.length;i++) {
 divs[i].onclick=function(){
 alert(i);
 }
 }
 </script>
 </body>
</html>

运行结果:

因为点击事件的函数内部使用外部的变量i一直在变化,当我们指定click事件时并没有保存i的副本,这样做也是为了提高性能,但达不到我们的目的,我们要让他执行的上下文保存i的副本,这种机制就是闭包。

修改后的代码:

<!DOCTYPE html>
<html>
 <head>
 <meta charset="UTF-8">
 <title>闭包</title>
 <style type="text/css">
 div {
 width: 100px;
 height: 100px;
 background: lightgreen;
 float: left;
 margin: 20px;
 font: 30px/100px "microsoft yahei";
 text-align: center;
 }
 </style>
 </head>
 <body>
 <div>a</div>
 <div>b</div>
 <div>c</div>
 <div>d</div>
 <div>e</div>
 <div>f</div>
 <div>g</div>
 <div>h</div>
 <div>i</div>
 <div>j</div>
 <script type="text/javascript">
 var divs=document.getElementsByTagName("div");
 for (var i=0;i<divs.length;i++) {
 divs[i].onclick=(function(n){
 return function(){
 alert(n); 
 }
 })(i);
 }
 </script>
 </body>
</html>

运行结果:

n是外部函数的值,但是内部函数(点击事件)需要使用,返回函数前的n被临时驻留在内存中给点击事件使用,简单说就是函数的执行上下文被保存起来,i生成了多个副本。

1.2、理解闭包

闭包概念:当一个内部函数被调用,就会形成闭包,闭包就是能够读取其他函数内部变量的函数,定义在一个函数内部的函,创建一个闭包环境,让返回的这个子程序抓住i,以便在后续执行时可以保持对这个i的引用。内部函数比外部函数有更长的生命周期;函数可以访问它被创建时所处的上下文环境。

Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量

二、对象

对象就是“键/值”对的集合并拥有一个连接到原型(prototype)对隐藏连接。

2.1、对象常量(字面量)

一个对象字面量就是包含在一对花括号中的零个或多个“键/值”对。对象字面量可以出现在任何允许表达式出现的地方。

对象的定义:

//空对象
 var obj1={};
 //对象中的属性
 var obj2={name:"foo",age:19};
 var obj3={"nick name":"dog"};
 //对象中的方法
 var obj4={
 price:99,
 inc:function(){
 this.price+=1;
 }
 }

对象中可包含的内容:

对象常量可以出现在任何允许表达式出现的地方,对象、数组、函数可以相互间嵌套,形式可以多种多样。对象的值可以是:数组,函数,对象,基本数据类型等。

//对象中可包含的内容
 var obj5 = [{
 name: "jack"
 }, {
 name: "lucy", //常量
 hobby:["读书","上网","代码"], //数组
 friend:{name:"mark",height:198,friend:{}}, //对象
 show:function(){ //函数
 console.log("大家好,我是"+this.name);
 }
 }];
 //对象中的this是动态的,指向的是:调用者
 obj5[1].show();

输出:大家好,我是lucy

2.2、取值

方法一:直接使用点号运算

 //3取值
 var obj6={"nick name":"pig",realname:"Rose"};
 console.log(obj6.realname);
 //console.log(obj6.nick name); 错误

方法二:使用索引器,当对象中的key有空格是

 //3取值
 var obj6={"nick name":"pig",realname:"Rose"};
 console.log(obj6["realname"]);
 console.log(obj6["nick name"]);

2.3、枚举(遍历)

方法一:

var obj7={weight:"55Kg","nick name":"pig",realname:"Rose"};
 for (var key in obj7) {
 console.log(key+":"+obj7[key]);
 }

运行结果:

输出顺序是不能保证的。

2.4、更新与添加

如果对象中存在属性就修改对应值,如果不存在就添加。对象通过引用传递,它们永远不会被复制

var obj8={realname:"King"};
 obj8.realname="Queen"; //修改
 obj8.weight=1000; //添加属性
 obj8.show=function() //添加方法
 {
 console.log(this.realname+","+this.weight);
 }
 obj8.show();

输出:

Queen,1000

var obj8={realname:"King"};
 obj8.realname="Queen"; //修改
 obj8.weight=1000; //添加属性
 obj8.show=function() //添加方法
 {
 console.log(this.realname+","+this.weight);
 }
 obj8.show();
 
 //引用
 var obj9=obj8; //obj9指向obj8的引用
 obj9.realname="Jack";
 obj8.show();

输出:

2.5、对象的原型

javascript是一种动态语言,与C#和Java这样的静态语言是不一样的;javascript并没有严格的类型,可以简单认为javascript是由对象组成的,对象间连接到原型(prototype)实现功能的扩展与继承。每个对象都链接到一个原型对象,并且可以从中继承属性,所有通过常量(字面量)创建的对象都连接到Object.prototype,它是JavaScript中的顶级(标配)对象,类似高级语言中的根类。

现在我们修改系统中的Object对象,添加一个创建方法,指定要创建对象的原型,实现类似继承功能:

<script type="text/javascript">
 if(typeof Object.beget !== "function")
 {
 Object.create = function(o) {
  //构造函数,用于创建对象
  var F = function() {};
  //指定由构造函数创建的对象的原型
  F.prototype = o;
  //调用构造方法创建新对象
  return new F();
 }
 }
 var rose={
 name:"rose",
 show:function(){
  console.log("姓名:"+this.name);
 }
 };
 rose.show(); //输出
 var lucy=Object.create(rose); //简单认为是:创建一个对象且继承rose
 lucy.name="lucy"; //重写
 lucy.show();
 </script>

运行结果:

原型关系是一种动态关系,如果修改原型,该原型创建的对象会受到影响。

var lucy=Object.create(rose); //简单认为是:创建一个对象且继承rose
 lucy.name="lucy"; //重写
 
 var jack=Object.create(rose);
 jack.name="jack";
 
 //修改原型中的方法
 rose.show=function(){
 console.log("姓名->"+this.name);
 }
 
 lucy.show();
 jack.show();

结果:

关于原型在函数中会再讲到。

2.6、删除

 //删除属性
 delete mark.name; 
 //调用方法,输出:姓名:undefined
 mark.show(); 
 //删除函数
 delete mark.show; 
 //错误,mark.show is not a function
 mark.show();

删除不用的属性是一个好习惯,在某些情况下可能引发内存泄漏。

2.7、封装

使用对象封装的好处是可以减少全局变量的污染机会,将属性,函数都隶属一个对象。

封装前:

var name="foo"; //name是全局的,被暴露
 i=1; //全局的,没有var关键字声明的变量是全局的,与位置关系不大
 function show(){ //show 是全局的,被暴露
 console.log("name->"+name);
 console.log(++i);
 }
 //i是全局的 2
 show(); 
 //3
 show();

封装后:

//对外只暴露bar,使用闭包封装
 var bar=function(){
 var i=1;
 return{
  name:"bar",
  show:function(){
  console.log("name->"+this.name);
  console.log(++i);
  }
 };
 }; 
 var bar1=bar();
 //2
 bar1.show();
 //3
 bar1.show(); 
 var bar2=bar();
 //2,因为被封装,且闭包,i是局部私有的
 bar2.show();

运行结果:

三、函数

javascript中的函数就是对象,对象就是“键/值”对的集合并拥有一个连接到原型对隐藏连接。

3.1、参数对象 (arguments)

第一个函数中有一个默认对象叫arguments,类似数组,但不是数组,该对象是传递给函数的参数。

<script type="text/javascript">
 function counter(){
 var sum=0;
 for(var i=0;i<arguments.length;i++){
  sum+=arguments[i];
 }
 return sum;
 }
 console.log(counter(199,991,1,2,3,4,5));
 console.log(counter());
 </script>

运行结果:

1205

0

这里的arguments是一个隐式对象,不声明也在函数中,内部函数可以访问外部函数的任意内容,但是不能直接访问外部函数的arguments与this对象。

function f1()
 {
 console.log(arguments.length);
 f2=function()
 {
  console.log(arguments.length);
 }
 return f2;
 }
 var f=f1(1,2,3);
 f();

运行结果:

3

0

3.2、构造函数

在javascript中对象构造函数可以创建一个对象。

<script type="text/javascript">
 /*构造函数*/
 //可以简单的认为是一个类型的定义
 function Student(name,age){
  this.name=name;
  this.age=age;
  this.show=function(){
  console.log(this.name+","+this.age);
  }
 }
 //通过new关键字调用构造函数,创建一个对象tom
 var rose=new Student("rose",18);
 var jack=new Student("jack",20);
 
 rose.show();
 jack.show();
 </script>

3.3、函数调用

3.3.1、call

调用一个对象的一个方法,以另一个对象替换当前对象

call([thisObj[,args])

hisObj 可选项。将被用作当前对象的对象。args 将被传递方法参数序列。

call 方法可以用来代替另一个对象调用一个方法。call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。

示例:

/*构造函数*/
 function Student(name,age){
  this.name=name;
  this.age=age;
 }
 show=function(add){
  console.log(add+":"+this.name+","+this.age);
 }
 //通过new关键字调用构造函数,创建一个对象tom
 var rose=new Student("rose",18);
 var jack=new Student("jack",20);
 //调用show方法,指定上下文,指定调用对象,this指向rose,“大家好是参数”
 show.call(rose,"大家好");
 show.call(jack,"Hello");

运行结果:

call方法中的参数都可以省去,第1个参数表示在哪个对象上调用该方法,或this指向谁,如果不指定则会指向window对象。

示例:

 var name="无名";
 var age=18;
 show.call();

结果:

undefined:无名,18

3.3.2、apply

apply([thisObj[,argArray]])

应用某一对象的一个方法,用另一个对象替换当前对象,与call类似。

如果 argArray 不是一个有效的数组或者不是arguments对象,那么将导致一个 TypeError。

如果没有提供 argArray 和 thisObj 任何一个参数,那么 Global 对象将被用作 thisObj, 并且无法被传递任何参数。

对于第一个参数意义都一样,但对第二个参数:

apply传入的是一个参数数组,也就是将多个参数组合成为一个数组传入,而call则作为call的参数传入(从第二个参数开始)。

如 func.call(func1,var1,var2,var3)对应的apply写法为:func.apply(func1,[var1,var2,var3])

同时使用apply的好处是可以直接将当前函数的arguments对象作为apply的第二个参数传入

示例代码:

/*构造函数*/
 function Student(name,age){
  this.name=name;
  this.age=age;
 }
 show=function(greeting,height){
  console.log(greeting+":"+this.name+","+this.age+","+height);
 }
 //通过new关键字调用构造函数,创建一个对象tom
 var rose=new Student("rose",18);
 var jack=new Student("jack",20);
 //调用show方法,指定上下文,指定调用对象,this指向rose,“大家好是参数”
 show.apply(rose,["大家好","178cm"]);
 show.apply(jack,["Hello","188cm"]);

运行结果:

从上面的示例中可以发现apply的第2个参数是一个数组,数组中的内容将映射到被调用方法的参数中,如果单这样看发现不如call方便,其实如果直接取方法的参数arguments则apply要方便一些。通过简单的变化就可以替代call。

 function display(){
 show.apply(jack,arguments);
 }
 display("hi","224cm");

结果:

hi:jack,20,224cm

javascript里call和apply操作符可以随意改变this指向

如果在javascript语言里没有通过new(包括对象字面量定义)、call和apply改变函数的this指针,函数的this指针都是指向window的。

关于this指针,我的总结是:是谁调用的函数,那么这个函数中的this指针就是它;如果没有明确看出是谁调用的,那么应该就是window调用的,那么this指针就是window。

3.3.3、caller

在一个函数调用另一个函数时,被调用函数会自动生成一个caller属性,指向调用它的函数对象。如果该函数当前未被调用,或并非被其他函数调用,则caller为null。

在JavaScript的早期版本中,Function对象的caller属性是对调用当前函数的函数的引用

function add()
 {
 console.log("add被调用");
 //add方法的调用函数,如果调用add方法的不是函数则为null
 console.log(add.caller);
 }
 function calc(){
 add();
 }
 //直接调用add方法
 add(); 
 //间接通过calc方法调用
 calc();

运行结果:

caller与this还是有区别的,this是指调用方法的对象,而caller是指调用函数的函数。

<script type="text/javascript">
 function add(n)
 {
 console.log("add被调用");
 if(n<=2){
 return 1;
 }
 return add.caller(n-1)+add.caller(n-2);
 }
 function calc(n){
 console.log("calc被调用");
 return add(n);
 }
 //1 1 2
 console.log(calc(3));
 </script>

结果:

3.3.4、Callee

当函数被调用时,它的arguments.callee对象就会指向自身,也就是一个对自己的引用

function add(n1,n2){
  console.log(n1+n2);
  //arguments.callee(n1,n2); //指向add方法
  return arguments.callee;
 }
 add(1,2)(3,4)(5,6)(7,8)(8,9);

运行结果:

当第1次调用add方法时输入3,立即将函数返回再次调用,每次调用后又返回自己,这样可以实现链式编程。

3.5、立即执行函数表达式 (IIFE)

IIFE即Immediately-Invoked Function Expression,立即执行函数表达式

3.5.1、匿名函数与匿名对象

匿名函数就是没有名称的函数,javascript中经常会使用匿名函数实现事件绑定,回调,实现函数级的私有作用域,如下所示:

 function(){
 console.log("这是一个匿名函数");
 };

匿名对象:

 {
 name:"foo",
 show:function(){
 console.log(this.name);
 }
 }

没有名称的匿名函数也叫函数表达式,它们间是有区别的。

3.5.2、函数与函数表达式

下面是关于函数与函数表达式定义时的区别

a)、函数定义(Function Declaration)

function Identifier ( Parameters ){ FunctionBody }

function 函数名称(参数){函数主体}

在函数定义中,参数(Parameters)标识符(Identifier )是必不可少的。如果遗漏,会报提示错误:

代码:

 function(){
 console.log("这是一个匿名函数");
 };

结果:

b)、函数表达式(Function Expression)

function Identifier(Parameters){ FunctionBody }

函数表达式中,参数和标识符都是可选的,与函数定义的区别是标识符可省去。

其实,"function Identifier(Parameters){ FunctionBody }"并不是一个完整的函数表达式,完整的函数的表达式,需要一个赋值操作。

比如: var name=function Identifier(Parameters){ FunctionBody }

3.5.3、立即执行函数表达式与匿名对象

 //1 正常定义函数
 function f1(){
 console.log("正常定义f1函数");
 };
 //2 被误解的函数表达式
 function(){
 console.log("报错Unexpected token (");
 }();
 //3 IIFE,括号中的内容被解释成函数表达式
 (function(){
 console.log("IIFE,正常执行");
 })();
 //4 函数表达式
 var f2=function(){
 console.log("这也被视为函数表达式");
 };

第3种写法为什么这样就能立即执行并且不报错呢?因为在javascript里,括号内部不能包含语句,当解析器对代码进行解释的时候,先碰到了(),然后碰到function关键字就会自动将()里面的代码识别为函数表达式而不是函数声明。

如果需要将函数表达式或匿名对象立即执行,可以使用如下方法:

<!DOCTYPE html>
<html>
 <head>
 <meta charset="UTF-8">
 <title>IIFE</title>
 </head>
 <body>
 <script type="text/javascript">
 //调用匿名函数
 (function() {
 console.log("这是一个函数表达式");
 })();
 //调用匿名对象
 ({
 name: "foo",
 show: function() {
  console.log(this.name);
 }
 }).show();
 console.log({
 a: 1
 }.a);
 console.log({
 a: function() {}
 }.a());
 </script>
 </body>
</html>

运行结果:

3.5.4、各种IIFE的写法

//最常用的两种写法
(function(){ /* code */ }()); // 老师推荐写法
(function(){ /* code */ })(); // 当然这种也可以
// 括号和JS的一些操作符(如 = && || ,等)可以在函数表达式和函数声明上消除歧义
// 如下代码中,解析器已经知道一个是表达式了,于是也会把另一个默认为表达式
// 但是两者交换则会报错
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"等运算符将其转换为函数表达式,然后再加"()"立即执行。

3.5.5、参数

函数表达式也是函数的一种表达形式,同样可以像函数一样使用参数,如下所示:

 (function (n){
 console.log(n);
 })(100);

输出:100

其实通过IIFE还能形成一个类似的块级作用域,当块内的程序在使用外部对象时将优先查找块内的对象,再查找块外的对象,依次向上。

 (function(win,undfd){
 win.console.log("Hello"==undfd);
 })(window,undefined);

3.5.6、添加分号

为了避免与其它的javascript代码产生影响后报错,常常会在IIFE前增加一个分号,表示前面所有的语句都结束了,开始新的一语句。

 var k=100
 (function (n){
 console.log(n);
 })(k);

上面的脚本会报错,因为javascript解释器会认为100是函数名。

 var k=100
 ;(function (n){
 console.log(n);
 })(k);

这样就正确了,在javascript中一行语句的结束可以使用分号,也可以不使用分号,因为一般的自定义插件会使用IIFE,这是一段独立的代码,在应用过程中不能保证用户会加上分号,所以建议在IIFE前加上分号。

3.5.7、IIFE的作用

1)、提高性能

减少作用域查找时间。使用IIFE的一个微小的性能优势是通过匿名函数的参数传递常用全局对象window、document、jQuery,在作用域内引用这些全局对象。JavaScript解释器首先在作用域内查找属性,然后一直沿着链向上查找,直到全局范围。将全局对象放在IIFE作用域内提升js解释器的查找速度和性能。

function(window, document, $) {
}(window, document, window.jQuery);

2)、压缩空间

通过参数传递全局对象,压缩时可以将这些全局对象匿名为一个更加精简的变量名

function(w, d, $) { 
}(window, document, window.jQuery);

3)、避免冲突

 匿名函数内部可以形成一个块级的私有作用域。

4)、依赖加载

可以灵活的加载第三方插件,当然使用模块化加载更好(AMD,CMD),示例如下。

A.html与B.html文件同时引用公用的common.js文件,但是只有A.html需要使用到StuObj对象,B.html不需要,但使用其它方法。

Student.js

var StuObj = {
 getStu: function(name) {
 return new Student(name);
 }
}
/*构造函数*/
function Student(name) {
 this.name = name;
 this.show = function() {
 console.log("Hello," + this.name);
 }
}

Common.js

function other1() {}
function other2() {}
(function($) {
 if($) {
 $.getStu("Tom").show();
 }
})(typeof StuObj=="undefined"?false:StuObj);

A.HTML

<!DOCTYPE html>
<html>
 <head>
 <meta charset="UTF-8">
 <title>A</title>
 </head>
 <body>
 <script src="js/Student.js" type="text/javascript" charset="utf-8"></script>
 <script src="js/common.js" type="text/javascript" charset="utf-8"></script>
 </body>
</html>

B.HTML

<!DOCTYPE html>
<html>
 <head>
 <meta charset="UTF-8">
 <title></title>
 </head>
 <body>
 <script src="js/common.js" type="text/javascript" charset="utf-8"></script>
 <script type="text/javascript">
 other1();
 </script>
 </body>
</html>

3.5.8、IIFE的变形

也许有人会说IIFE将参数放在最后,需要移动到文档的末尾才能看到参数,比较麻烦,那么可以将IIFE变形为如下形式:

(function(n){
 console.log(n);
 
 
 
 
 
 
 
 //认为这里有30000代码
 
 
 
 
 
 
 
 }(100));

如果中间有很长的代码,参数100只有到文档的末尾才可以看得到,变形后的结果:

(function(exp){
 exp(100);
 }(function(n){
 console.log(n);
 //认为这里有30000代码
 }));

修改后的代码中有两个函数表达式,一个作为参数,就是我们主要要完成的功能向控制台输出数字,另一个作来IIFE立即执行的函数,主要的功能函数变成的IIFE的参数了。

(function(win, doc, $) {
 }(window, document, jQuery));
 (
 function(library) {
  library(window, document, window.jQuery);
 }
 (function(window, document, $) {
 })
 );

bootstrap的写法:

+function(yourcode) {
 yourcode(window.jQuery, window, document);
 }(function($, window, document) {
  $(function() {}); //jQueryDOM加载完成事件
 });

结合call或apply的写法:

 (function(x){console.log(x)}).call(window,888);
 (function(x){console.log(x)}).apply(window,[999]);

输出:888 999

四、示例下载

http://wd.oudahe.com:81//201612/yuanma/javascript003-master_jb51.rar

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持编程小技巧!

代码技巧

转载请关注公众号:代码技巧 回复:授权

本文链接地址:https://www.oudahe.com/p/409/