JavaScript学习总结(三)——闭包、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的引用。内部函数比外部函数有更长的生命周期;函数可以访问它被创建时所处的上下文环境。
示例1:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>闭包</title> </head> <body> <script type="text/javascript"> //允许函数中嵌套函数 //内部函数允许调用外部函数的变量 //闭包就是能够读取其他函数内部变量的函数,内部函数和执行的上下文 var foo=function(){ var n=1; return function(){ n=n+1; console.log(n); } } var bar=foo(); bar(); //2 bar(); //3 var foobar=foo(); foobar(); //2 foobar(); //3 </script> </body> </html>
运行结果:
Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量
定义:闭包是指可以访问另一个函数作用域变量的函数,一般是定义在外层函数中的内层函数。闭包就是能够读取其他函数内部变量的函数。定义在一个函数内部的函数。
作用:局部变量无法共享和长久的保存,而全局变量可能造成变量污染,所以我们希望有一种机制既可以长久的保存变量又不会造成全局污染。
特点:占用更多内存;不容易被释放
用法:变量既想反复使用,又想避免全局污染如何使用?
- 1.定义外层函数,封装被保护的局部变量。
- 2.定义内层函数,执行对外部函数变量的操作。
- 3.外层函数返回内层函数的对象,并且外层函数被调用,结果保存在一个全局的变量中。
示例2:
var getNum; function getCounter() { var n = 1; var inner = function () { return n++; } return inner; } getNum = getCounter(); console.log(getNum()); console.log(getNum());
结果:1 2
示例3:
function f1(){ var n=999; nAdd=function(){n+=1} function f2(){ alert(n); } return f2; } var result=f1(); result(); // 999 nAdd(); result(); // 1000
结果:999 1000
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
1.3、闭包测试
如果你能理解下面三段代码的运行结果,应该就算理解闭包的运行机制了。
代码片段一:
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ return function(){ return this.name; }; } }; alert(object.getNameFunc()());
代码片段二:
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ var that = this; return function(){ return that.name; }; } }; alert(object.getNameFunc()());
示例三:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>闭包</title> </head> <body> <div id="div1"> <h2>h2点击我看看</h2> <h2>h2点击我看看</h2> <h2>h2点击我看看</h2> <h2>h2点击我看看</h2> <h2>h2点击我看看</h2> </div> <script type="text/javascript"> //闭包:使用外部函数内部变量的函数。 var items = document.getElementsByTagName("h2"); for(var i = 0; i < items.length; i++) { items[i].onclick =(function(n){ return function() { alert(n + 1); } })(i); } </script> </body> </html>
结果:
面试题:
function fun(n, o) { console.log(o); return { fun: function (m) { return fun(m, n); } }; } var a = fun(0); // ? a.fun(1); // ? a.fun(2); // ? a.fun(3); // ? var b = fun(0).fun(1).fun(2).fun(3); // ? var c = fun(0).fun(1); // ? c.fun(2); // ? c.fun(3); // ?
答案:
一、 //undefind //0 //0 //0 二、 //undefind //0 //1 //2 三、 //undefind //0 //1 //1
1.4、小结
闭包就是使用外部函数内部变量的函数
注意事项:
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
二、对象
对象就是“键/值”对的集合并拥有一个连接到原型(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]); }
运行结果:
输出顺序是不能保证的。
示例二:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>对象</title> </head> <body> <h2>对象</h2> <script> var phone={"name":"Mi Plus5","price":999,"color":"white"}; //直接访问 console.log(phone.name); console.log(phone["price"]); //迭代 for(var key in phone){ console.log(key+"->"+phone[key]); } //迭代window for(var key in this){ console.log(key); console.log(this[key]); } </script> </body> </html>
结果:
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中的顶级(标配)对象,类似高级语言中的根类。
创建对象的三种方法:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <body> <script type="text/javascript"> //1、对象字面量创建对象 var o={name:"goodcat"}; //2、通过Object创建对象 var obj=new Object(); obj.name="badcat"; //3、构造函数,类,Cat就是一个类型 function Cat(name){ this.name=name; } //对象(通过构造方法创建的对象) var mycat=new Cat("tom"); console.log(mycat.name); //类型判断 console.log(mycat instanceof Cat); console.log(mycat instanceof Object); </script> </body> </html>
理解Object,Function,prototype,__proto__,constractor(构造方法)之间的关系非常重要。
通过修改原型实现扩展方法,对象的prototype是不允许直接访问的,可以使用__proto__访问:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>对象</title> </head> <body> <h2>原型</h2> <script> Object.prototype.o=function(){ alert(this); console.log(this); } var str="Hello JavaScript!"; str.o(); "Hello Prototype!".o(); var phone={"name":"Mi Plus5","price":999,"color":"white"}; phone.o(); console.log(phone.__proto__); //Object console.log(phone.prototype); //对象的prototype是不允许直接访问的,可以使用__proto__访问 </script> </body> </html>
结果:
示例2:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <body> <script type="text/javascript"> //1、对象字面量创建对象 var o = { name: "goodcat" }; //2、通过Object创建对象 var obj = new Object(); obj.name = "badcat"; //3、构造函数,类,Cat就是一个类型 function Cat(name) { this.name = name; } //对象(通过构造方法创建的对象) var mycat = new Cat("tom"); var okcat = new Cat("ok"); console.log(mycat.name); //对象的prototype是不允许直接访问的(类型的是可以访问的),可以使用__proto__访问 console.log(mycat.prototype); console.log(mycat.__proto__); //非标准,chrome //通过修改原型实现扩展方法,类型的prototype是可以访问的 Cat.prototype.show = function() { console.info("这是一只叫" + this.name + "的猫"); } mycat.show(); okcat.show(); //在Object的原型中添加了一个out方法 Object.prototype.out=function(){ alert(JSON.stringify(this)); } mycat.out(); okcat.out(); (new Date()).out(); "Hello".out(); Window.out(); </script> </body> </html>
结果:
javascript对象藏宝图:
在JavaScript中,原型也是一个对象,通过原型可以实现对象的属性继承,JavaScript的对象中都包含了一个"Prototype"内部属性,这个属性所对应的就是该对象的原型。
"Prototype"作为对象的内部属性,是不能被直接访问的。所以为了方便查看一个对象的原型,Firefox和Chrome中提供了"__proto__"这个非标准(不是所有浏览器都支持)的访问器(ECMA引入了标准对象原型访问器"Object.getPrototype(object)")。
(1)、所有构造器/函数的__proto__都指向Function.prototype,它是一个空函数(Empty function)
(2)、所有对象的__proto__都指向其构造器的prototype
(3)、对于所有的对象,都有__proto__属性,这个属性对应该对象的原型
(4)、对于函数对象,除了__proto__属性之外,还有prototype属性,当一个函数被用作构造函数来创建实例时,该函数的prototype属性值将被作为原型赋值给所有对象实例(也就是设置实例的__proto__属性)
使用原型实现继承:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>对象</title> </head> <body> <h2>原型</h2> <script> //机器 父类 var Machine={ weight:1355 }; //构造方法,类 function Car(_name){ this.name=_name; } //Car的原型是机器 Car.prototype=Machine; //创建对象 var bmw=new Car("宝马"); var benz=new Car("奔驰"); console.log(bmw.name); console.log(bmw.weight); </script> </body> </html>
结果:
现在我们修改系统中的Object对象,添加一个创建方法,指定要创建对象的原型,实现类似继承功能:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>对象</title> </head> <body> <h2>原型</h2> <script> alert(typeof Object.beget); if(typeof Object.beget==="undefined"){ Object.beget=function(proto){ //构造方法,F是一个类型 var F=function(){}; //指定类型F的原型 F.prototype=proto; //创建一个F类型的对象 return new F(); } } //机器 var Machine={ name:"机器", show:function(){ console.log("机器的名称是:"+this.name); } }; //创建对象,指定原型 var bmw=Object.beget(Machine); bmw.name="宝马"; bmw.show(); console.log(bmw); </script> </body> </html>
运行结果:
原型关系是一种动态关系,如果修改原型,该原型创建的对象会受到影响。
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();
运行结果:
2.8、对象的浅拷贝与深拷贝
浅拷贝和深拷贝都是对于JS中的引用类型而言的,浅拷贝就只是复制对象的引用(堆和栈的关系,原始(基本)类型Undefined,Null,Boolean,Number和String是存入堆,直接引用,object array 则是存入桟中,只用一个指针来引用值),如果拷贝后的对象发生变化,原对象也会发生变化。只有深拷贝才是真正地对对象的拷贝。
2.8.1、浅拷贝
默认是浅拷贝,只是将地址进行了复制,示例如下:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <body> <script type="text/javascript"> //浅拷贝 var obj1={name:"cat"}; var obj2=obj1; obj2.name="pig"; console.log(obj1.name); console.log(obj2.name); </script> </body> </html>
结果:pig
2.8.2、深拷贝
深拷贝就是对目标的完全拷贝,不像浅拷贝那样只是复制了一层引用,就连值也都复制了。
只要进行了深拷贝,不会相互影响。
实现深拷贝的方法主要是三种:
1、利用 JSON 对象中的 parse 和 stringify
就是说如果对象中含有一个函数时(很常见),就不能用这个方法进行深拷贝
var obj1 = { name: "cat", show:function(){ console.log(this.name); } }; var obj2 = JSON.parse(JSON.stringify(obj1)); obj2.name = "pig"; console.log(obj1.name); console.log(obj2.name); obj1.show(); obj2.show(); //函数被丢失
结果:
2、利用递归来实现每一层都重新创建对象并赋值
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <body> <script type="text/javascript"> var obj1 = { name: "cat", show: function() { console.log("名称:"+this.name); } }; var obj2 = deepClone(obj1); obj2.name = "pig"; console.log(obj1.name); console.log(obj2.name); obj1.show(); obj2.show(); function deepClone(obj) { let objClone = Array.isArray(obj) ? [] : {}; if(obj && typeof obj === "object") { for(key in obj) { if(obj.hasOwnProperty(key)) { //判断ojb子元素是否为对象,如果是,递归复制 if(obj[key] && typeof obj[key] === "object") { objClone[key] = deepClone(obj[key]); } else { //如果不是,简单复制 objClone[key] = obj[key]; } } } } return objClone; } </script> </body> </html>
运行结果:
3、ES6 中 引入了Object.assgn 方法展开运算符也能实现对对象的拷贝。
赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;
JSON.stringify 实现的是深拷贝,但是对目标对象有要求(非 undefined,function);
若想真正意义上的深拷贝,请递归。
2.9、对象创建(new)的原理
在 JavaScript 中,使用 new 关键字后,意味着做了如下四件事情:
1、创建一个新的对象,这个对象的类型是 object;
2、设置这个新的对象的内部、可访问性和[[prototype]]属性为构造函数(指prototype.construtor所指向的构造函数)中设置的;
3、执行构造函数,当this关键字被提及的时候,使用新创建的对象的属性; 返回新创建的对象(除非构造方法中返回的是‘无原型’)。
4、在创建新对象成功之后,如果调用一个新对象没有的属性的时候,JavaScript 会延原型链向止逐层查找对应的内容。这类似于传统的‘类继承’。
注意:在第二点中所说的有关[[prototype]]属性,只有在一个对象被创建的时候起作用,比如使用 new 关键字、使用 Object.create 、基于字面意义的(函数默认为 Function.prototype ,数字默认为 Number.prototype 等)。它只能被Object.getPrototypeOf(someObject) 所读取。没有其他任何方式来设置或读取这个值。
示例:
1 function Foo(){} 2 var foo = new Foo();
每一个函数中都会有一个叫prototype的属性,类型是object,即一个引用对象。
每一个对象中都会有一个叫__proto__的属性,类型是object,是一个引用对象。
1、当JavaScript引擎执行new操作时,会马上开辟一个块内存,创建一个空对象(并将this指向这个对象)。
2、执行构造函数Foo(),对这个空对象进行构造(构造函数里有什么属性和方法都一一给这个空白对象装配上去,这就是为何它叫构造函数了)。
3、给这个空对象添加了一个叫__proto__的属性,而且这个__proto__指向Foo()的prototype对象。换句话说,就是__proto__ = prototype;
2.10、Object与Object.property
2.10.1、Object
JavaScript中的所有对象都来自 Object;所有对象从Object.prototype继承方法和属性,尽管它们可能被覆盖。例如,其他构造函数的原型将覆盖 constructor 属性并提供自己的 toString() 方法。Object 原型对象的更改将传播到所有对象,除非受到这些更改的属性和方法将沿原型链进一步覆盖。
Object 构造函数为给定值创建一个对象包装器。如果给定值是 null 或 undefined,将会创建并返回一个空对象,否则,将返回一个与给定值对应类型的对象。
当以非构造函数形式被调用时,Object 等同于 new Object()。
Object.assign()
通过复制一个或多个对象来创建一个新的对象。
Object.create()
使用指定的原型对象和属性创建一个新对象。
Object.defineProperty()
给对象添加一个属性并指定该属性的配置。
Object.defineProperties()
给对象添加多个属性并分别指定它们的配置。
Object.entries()
返回给定对象自身可枚举属性的 [key, value] 数组。
Object.freeze()
冻结对象:其他代码不能删除或更改任何属性。
Object.getOwnPropertyDescriptor()
返回对象指定的属性配置。
Object.getOwnPropertyNames()
返回一个数组,它包含了指定对象所有的可枚举或不可枚举的属性名。
Object.getOwnPropertySymbols()
返回一个数组,它包含了指定对象自身所有的符号属性。
Object.getPrototypeOf()
返回指定对象的原型对象。
Object.is()
比较两个值是否相同。所有 NaN 值都相等(这与==和===不同)。
Object.isExtensible()
判断对象是否可扩展。
Object.isFrozen()
判断对象是否已经冻结。
Object.isSealed()
判断对象是否已经密封。
Object.keys()
返回一个包含所有给定对象自身可枚举属性名称的数组。
Object.preventExtensions()
防止对象的任何扩展。
Object.seal()
防止其他代码删除对象的属性。
Object.setPrototypeOf()
设置对象的原型(即内部 [[Prototype]] 属性)。
Object.values()
返回给定对象自身可枚举值的数组。
2.10.2、Object.property
几乎所有的 JavaScript 对象都是 Object 的实例;一个典型的对象继承了Object.prototype的属性(包括方法),尽管这些属性可能被遮蔽(亦称为覆盖)。但是有时候可能故意创建不具有典型原型链继承的对象,比如通过Object.create(null)创建的对象,或者通过Object.setPrototypeOf方法改变原型链。
改变Object原型,会通过原型链改变所有对象;除非在原型链中进一步覆盖受这些变化影响的属性和方法。这提供了一个非常强大的、但有潜在危险的机制来覆盖或扩展对象行为。
Object.prototype.constructor
特定的函数,用于创建一个对象的原型。
Object.prototype.__proto__
指向当对象被实例化的时候,用作原型的对象。
Object.prototype.__noSuchMethod__
当未定义的对象成员被调用作方法的时候,允许定义并执行的函数。
Object.prototype.__defineGetter__()
关联一个函数到一个属性。访问该函数时,执行该函数并返回其返回值。
Object.prototype.__defineSetter__()
关联一个函数到一个属性。设置该函数时,执行该修改属性的函数。
Object.prototype.__lookupGetter__()
返回使用 __defineGetter__ 定义的方法函数 。
Object.prototype.__lookupSetter__()
返回使用 __defineSetter__ 定义的方法函数。
Object.prototype.hasOwnProperty()
返回一个布尔值 ,表示某个对象是否含有指定的属性,而且此属性非原型链继承的。
Object.prototype.isPrototypeOf()
返回一个布尔值,表示指定的对象是否在本对象的原型链中。
Object.prototype.propertyIsEnumerable()
判断指定属性是否可枚举,内部属性设置参见 ECMAScript [[Enumerable]] attribute 。
Object.prototype.toSource()
返回字符串表示此对象的源代码形式,可以使用此字符串生成一个新的相同的对象。
Object.prototype.toLocaleString()
直接调用 toString()方法。
Object.prototype.toString()
返回对象的字符串表示。
Object.prototype.unwatch()
移除对象某个属性的监听。
Object.prototype.valueOf()
返回指定对象的原始值。
Object.prototype.watch()
给对象的某个属性增加监听。
三、函数 Function
Function 构造函数 创建一个新的Function对象。 在 JavaScript 中, 每个函数实际上都是一个Function对象。
var add = new Function('m', 'n', 'return m + n'); console.log(add(1, 2));
使用Function构造器生成的Function对象是在函数创建时解析的。这比你使用函数声明或者函数表达式(function)并在你的代码中调用更为低效,因为使用后者创建的函数是跟其他代码一起解析的。
所有被传递到构造函数中的参数,都将被视为将被创建的函数的参数,并且是相同的标示符名称和传递顺序。
javascript中的函数就是对象,对象就是“键/值”对的集合并拥有一个连接到原型对隐藏连接。
属性
arguments[]
一个参数数组,元素是传递给函数的参数。反对使用该属性。
caller
对调用当前函数的Function对象的引用,如果当前函数由顶层代码调用,这个属性的值为null。反对使用该属性。
length
在声明函数时指定的命名参数的个数。
prototype
一个对象,用于构造函数,这个对象定义的属性和方法由构造函数创建的所有对象共享。
方法
apply( )
将函数作为指定对象的方法来调用,传递给它的是指定的参数数组。
call( )
将函数作为指定对象的方法来调用,传递给它的是指定的参数。
toString( )
返回函数的字符串表示。
描述
函数是JavaScript的一种基本数据类型。注意,虽然可以用这里介绍的Function()构造函数创建函数对象, 但这样做效率不高,在大多数情况下,建议使用函数定义语句或函数直接量来定义函数。
在JavaScriptl.1及以后版本中,函数主体会被自动地给予一个局部变量arguments,它引用一个Arguments对象。该对象是一个数组,元素是传递给函数的参数值。
3.0、Function定义
Function实际上是对象,与其他引用类型一样具有属性和方法。Function可以通过三种方法进行定义,分别是函数声明语法定义,函数表达式定义和Function构造函数定义。
1.函数声明语法定义
function functionName(value1...){ //函数体 }
2.函数表达式定义
var functionName = function(value1...){ //函数体 }
3.Function构造函数定义
通过Function构造函数创建函数,可向构造函数中传入任意数量的参数,但值得注意的是传入的最后一个参数会作为函数体,而其他参数则作为参数传入函数中。用该方法去定义函数是不推荐使用的,因为该语法会导致解析两次代码,第一次解析常规ECMAScript代码,第二次解析传入构造函数的字符串,影响性能。
var functionName = new Function("value",...,"函数体");
示例:
var f2=new Function("n1","n2","return n1+n2;"); console.log(f2(1,2));
结果:3
注:函数是引入值类型,所以函数名仅仅是指向函数的指针,当使用函数名去赋值给另一个变量名时,仅仅复制的是一个指针。即在下列a设置为null时,仅将a存的指针消除而已,不会影响b调用函数。
var a = b = function(value1){ return value1; } a = null; b(1);
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.4、length
在声明函数时指定的命名参数的个数。
示例:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Function</title> </head> <body> <h2>Function - length</h2> <script> function f1(n1,n2) { console.log("实际带入的参数个数:"+arguments.length); } console.log("定义的命名参数个数:"+f1.length); f1(1); f1(1,2,3); </script> </body> </html>
结果:
3.5、立即执行函数表达式 (IIFE)
IIFE即Immediately-Invoked Function Expression,立即执行函数表达式,在 JavaScript 中每个函数被调用时,都会创建一个新的执行上下文。因为在函数里定义的变量和函数是唯一在内部被访问的变量,而不是在外部被访问的变量,当调用函数时,函数提供的上下文提供了一个非常简单的方法创建私有变量。
3.5.0、块级作用域与函数作用域
javascript没有块级作用域但有函数级作用域,可以使用IIFE模拟块级作用域。
任何一对花括号中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
作用域永远都是任何一门编程语言中的重中之重,因为它控制着变量与参数的可见性与生命周期。
块级作用域:任何一对花括号({和})中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
与函数作用域:函数作用域就好理解了,定义在函数中的参数和变量在函数外部是不可见的。
示例:
<script type="text/javascript"> function calc(){ for(var i=0;i<5;i++){ console.log(i); } console.log(i); }
calc(); </script>
结果:
你想的结果:
<script type="text/javascript"> function calc(){ for(var i=0;i<5;i++){ console.log(i); //0,1,2,3,4 } console.log(i); //报错 } calc(); </script>
实际结果:
解决方法,模拟一个块级作用域:
<script type="text/javascript"> function calc() { //IIFE (function() { for(var i = 0; i < 5; i++) { console.log(i); //0,1,2,3,4 } })(); console.log(i); //报错 } calc(); </script>
结果:
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 函数名称(参数){函数主体}
在函数定义中,函数名称是必不可少的,如果遗漏,会报提示错误:
代码:
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
四、示例下载
https://github.com/zhangguo5/javascript003.git
https://git.coding.net/zhangguo5/javascript_01.git
https://git.dev.tencent.com/zhangguo5/javascriptpro.git
五、视频
http://www.bilibili.com/video/av17173253/
六、作业
6.1、请扩展String类型增加trim方法,实现去掉字符串首尾空格,如:
“ abc ”.trim(); //abc
6.2、请定义一个动物(Animal)类型,并定义属性(name名称,food食物),定义方法eat吃,在方法中输出“小狗喜欢吃骨头!”
定义猫与狗类型,继承自Animal,增加show方法显示名称与喜欢的食物,完成测试。