一、定义:
1.函数声明 function func () {}
2.函数表达式 var func = function () {}
注意点:
var func = function test () {} func(); // ok test(); // 报错, test isn't defined
匿名函数表达式 和 命名函数表达式 区别
(1)命名函数表达式
function test () {} console.log(test.name); // test
(2)匿名函数表达式
var test = function func () {} console.log(func.name); // func is not defined console.log(test.name); // func
二、return作用
1.返回经过函数一系列处理的结果值
2.终止函数的运行
三、实参传递的数目和设定的形参数目相比,可多,可少,都不算错
为什么? 因为函数的形式上下文(一个对象)中有个名为arguments属性,其值为一个类数组,储存着所有传递过来的实参,所以调用函数传实参时直接将所有实参按形参名作为属性名存入argumengs这个类数组中,而不会去在意实参的数目和形参设定的数目是否一样
可通过funcName.length 查看形参数目, 通过arguments.length 查看实参数目
四、作用域
1、定义:变量(变量作用域又称为上下文)和函数生效(能被访问)的区域
2.作用于相关定义:
(1)执行期上下文:当函数执行时,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,自己的执行上下文被销毁(注意仅仅销毁自己本身的执行期上下文)。
(2)查找变量:从作用域链(scope chain的顶端依次向下查找。
3、通过函数来简单理解的作用域
如下js代码:
function a() { var aa = 'aa'; function b() { var bb = 'bb'; console.log(aa);
console.log(b); } return b; } var demo = a(); demo(); // aa function b(){...}
首先,在a函数定义时会生成一个将全局执行期上下文存入scope chain(作用域链),但不会产生自己本身的执行期上下文。
然后,在a函数执行后,会再次形成一个新的属于自己的scope chain(作用域链),同时将全局执行期上下文(执行期上下文是一个对象,为引用值)存入刚形成的scope chain顶部,然后再将自己本身的执行期上下文存入刚形成的scope chain顶部,也可这样理解,scope chain是一个数组,现在0序号位置存的是a函数自己本身的执行期上下文引用,1序号位置存的是全局执行器上下文引用(从数组头部插入)
(若在a函数在内部访问一个变量,则去scope chain中寻找该变量,先从0位置开始寻找,若0位置存储的执行期上下文没有该变量,则再看1序号位置存储的,直到找到该变量为止,最坏的情况,scope chain 中没有改变量的信息,则肯定是报错了呗,嘿嘿),
我们需要注意一个情况,在a函数执行的过程中在a函数内定义了一个函数b,所以b函数会和a函数一样形成scope chain,再将全局执行期上下文的引用、a的执行器上下文的引用按顺序存入scope chain中
(只有a函数执行,才会产生a函数自己本身的执行器上下文,才会b函数被定义, 也正因为如此,b函数定义时才能将a函数的执行期上下文存入自己的scope chain中, 所以最终在b函数内部才能打印出b函数,这是串行操作,中间一个环节断了都不行,这是我的个人理解,仅限参考,嘿嘿)
(最后最重要的,a函数执行的最终结果是将定义好的b函数返回给demo,此过程是将b函数的引用赋值给demo(所以demo函数执行打印的是b函数),需要注意的是,此赋值过程同时是变量demo将b函数定义时产生的scope chain也同样完整的继承了过去,即demo的scope chain中1和0序号位置分别存储了全局执行期上下文的引用的函数a的执行期上下文的引用,所以demo函数才能打印出a函数内定义的变量aa和a函数内定义的函数b)。
( 注意这里的存入,值得是scope chain该索引位置存的是执行期上下文对象的引用,可以形象的想象为scope chain该索引位置用箭头指向了执行期上下文)
最后一点,其实其中所说的执行期上下文就是预编译中所创建的AO对象(Activation Object),不懂预编译的可以去了解一下,将作用域和预编译一起理解感觉会好一些
五、闭包
1、定义:当内部函数(在函数内定义的函数)被保存到外部时,将会生成闭包。这是最常见的一种 情况而已,广义来说,就是一个作用域引用或保存了另一个作用域的值,导致另一个作用域 本该销毁被无法销毁,这就是闭包。
2、闭包是这样的:
仍然通过上边的例子解释:
function a() { var aa = 'aa'; function b() { var bb = 'bb'; console.log(aa); console.log(b); } return b; } var demo = a(); demo(); // aa function b(){...}
首先,问一个问题?你们是否发现在a函数中完毕后,按定义说a的执行器上下文应该被销毁啊,那为什么demo执行后仍然能打印出aa和b函数呢?
原因就是形成了闭包,导致a函数执行结束后a的执行期上下文没有被销毁,所有demo执行仍能正常打印,也就是由于函数b被return出来给了demo,使demo和a的执行期上下文有联系,也就是demo占用了a的执行期上下文,所以a执行完毕后a的执行期上下文不会被销毁,最重要的是demo执行结束后,demo本身的执行期上下文被销毁,但其连接的a的执行期上下文中存储了b函数,即b函数的引用(相当于demo),所以a的执行期上下文永远不会被销毁(可以通过赋值demo为null,表示demo不占用a的执行期上下文,那么a的执行期上下文就可以被销毁了),这就形成了闭包,这就是闭包的基本原理。
3.缺点:闭包会导致原有作用域链不释放,造成内存泄露。
解释: 上面本来a函数执行完毕后其执行期上下文应该被销毁的,但因为闭包原因没有被销毁,所以导致可使用的总内存量减少,即内存泄漏。
4.简单应用:
(1)实现公有变量:
eg: 函数累加器
// 闭包形成的累加器,变量num就类似java中说的共有变量 function bibao() { var num = 0; return function () { console.log(num++); } } var add1 = bibao(); add1(); //0 add1(); //1 add1(); //2 add1(); //3 add1(); //4
有于闭包原因,导致bibao函数的执行期上下文不会被销毁,所以num一直可以在之前基础上累加
(2)可以做缓存:
// 两个函数公用eater函数的作用域链 function eater() { var food; var obj = { eat: function () { if (food) { console.log('I eat ' + food); food = null; }else { console.log('There is nothing'); } }, push: function (myFood) { food = myFood; } } return obj; } var oEater = eater(); oEater.eat(); //There is nothing oEater.push('apple'); oEater.eat(); //I eat apple
(3)、实现封装,属性私有化
又是我们需要这样一些需求,我们希望函数内部的某些属性无法被外部写,仅仅只能被外部读而已等等。
最初,大家是共同制定了一个约定,那就是在函数内部不希望被外部写的属性钱加一个下划线,标示着该属性不能被其他人更改,但是该约定约束性不强。
后来,大家想到了一个方法,那就是用闭包来实现属性的私有化,此方法具有很强的约束性。如下demo举例:
function demo () { var num = 1, name = 'lyl'; // 和返回一个函数形成的闭包是一样的,只不过此处是返回多个函数,且多个函数用一个对象包装了。 // 该对象中的多个函数都占用了demo执行时产生的执行期上下文,导致demo执行时产生的执行期上下文不会被销毁,从而产生闭包 return { sayName: function () { console.log(name); }, sayNum: function () { console.log(num); } } } var obj = demo(); obj.sayName(); // lyl console.log(obj.num); //undefined
5.闭包的防范:闭包会导致多个执行函数共用一个公有变量,如果不是特殊需要,应尽量防止这种情况发生。
如下例子:arr数组中多个值公用一个test函数执行期上下文的变量 i,导致打印结果超出意料
function test() { var arr = []; for(var i = 0; i < 10; i++ ) { //arr中所有值对应一个执行期上下文,其中的i是随循环不断累加变化的,最终i固定为10不变,所以打印出的都为10 arr[i] = function () { console.log(i); } } return arr; } var retArr = test(); for(var i = 0, len = retArr.length; i < len; i++ ) { retArr[i](); } // 打印结果是10个10
解决方法: 利用立即执行函数(下面会讲),让arr数组中每个值都对应一个独立的执行期上下文,避免多个值公用一个执行期上下文的变量
function test() { var arr = []; for(var i = 0; i < 10; i++) { (function(j) { // 每次都将对应的i转换为j,所以每次arr[j]对应一个自己独有的执行期上下文,其中的j是固定不变的,所以能从0打印到9 // 即arr数组中有10个数,则分别对应10个独立的执行期上下文 arr[j] = function () { console.log(j); } } (i) ); } return arr; } var retArr = test(); for(var i = 0, len = retArr.length; i < len; i++) { retArr[i](); } // 打印结果为0 1 2 3 4 5 6 7 8 9
六、立即执行函数
1.定义:此类函数没有声明,在一次执行过后即释放。适合做初始化工作。(即此函数不需调用,直接执行)
2、使用形式如下:
// 立即执行函数基本形式 // 第一种,推荐 (function (形参) { //handle code } (实参) ); // 第二种 (function (形参) { // handle code } )(实参); // demo var ret = (function (a, b) { return a + b; } (1, 2) ); console.log(ret); //3
3、立即执行函数的原理:
为什么这样写,就可以执行了?大概原理如下:
其实重点在于小括号上,小括号()代表了执行,而()前放什么才可以执行呢?答案是表达式。 那表达式是什么呢?权威指南上关于表达式有这样一句话‘表达式是JavaScript的一个短语,JavaScript解释器会将其计算出一个结果’,简单来说只要是某一个类型的值,就可以看作表达式,当然此处关于表达式的理解是不全面的,但对于立即执行函数的立即来说是足够的了。所以函数执行就可以解释了,函数名是该函数的引用,为一个函数表达式,所以加上()后也就可以执行了。下面根据此理论来解释立即执行函数:
凡是()圈起来的都是表达式,所以(function (){})为一个表达式,然后再加上一个()则可以执行,所以实参放入后面那个括号中,形参放于function后的括号中也就合理了;而(function() {} ())此种形式可以算作立即执行函数的一个标准写法,省去了function (){}转换为表达式的时间,执行能更快一些。
除了常用的一些正规写法外,利用其原理,还有一些其他写法,同样能实现立即执行函数的效果。只要()前是表达式即可。如下:
// 成功的demo var demo1 = function func() {console.log('demo1')}(); //demo1, demo1被赋值为实名函数表达式 var demo2 = function () {console.log('demo2')}; // demo2, demo2被复制为匿名函数表达式 +function (){console.log('demo3')}; // demo3, 通过+运算符的隐士类型转换将+后面的转换为number原始表达式,所以也就可以执行了 console.log(typeof +function (){}); //number -function (){console.log('demo4')}; // demo4, 通过-运算符的隐士类型转换将-后面的转换为number原始表达式,所以也就可以执行了 console.log(typeof -function (){}); //number !function (){console.log('demo5')}; // demo5, 通过+运算符的隐士类型转换将+后面的转换为boolean原始表达式,所以也就可以执行了 console.log(typeof !function (){}); //boolean 1 && function (){console.log('demo6')}(); // demo6, 通过&&运算符的隐士类型转换将&&后面的转换为boolean原始表达式,所以也就可以执行了,但此处需要注意一点,
&&运算符仅仅判断是将值隐士转换为boolean类型的,但返回的是原值,因为都判定为true所以返回&&后面的原始值,否则返回&&前面的原始值 0 || function () {console.log('demo7')}(); // demo7, ||隐士类型转换为boolean原始表达式,所以可以执行,0 判定失败, 看||后面的,||后面的判定为true,返回该函数所以执行 // 失败的需要注意的demo *function (){console.log('demo1')}(); // 报错,以为*无法隐士类型转换 1 || function (){console.log('demo2')}(); // 报错,因为1判定为true,所以不看||后面的,直接返回1,所以无法执行 0 && function (){console.log('demo3')}(); // 报错,因为0判定为false,所以不看&&后面的,直接返回0,所以无法执行
七、预编译:
1、js运行三部曲:
(1)、语法分析(通篇分析查找是否存在低级语法错误,低级语法错误是直接可以看出的,不用执行就可以找出的)
*低级语法错误:
console.log('aa'); console.log('a'; //低级语法错误,不用执行就可以直接看出的 |* 影响上面代码的执行,也影响下面代码的执行 // 执行结果: missing ) after argument list
*逻辑语法错误:
console.log('aa'); console.log(a); //逻辑语法错误,无法直接看出,需要执行浏览器执行才能找出 |*不影响上面代码的执行,但影响下面代码的执行 //执行结果: //aa //a is not defined(…)
(2)、预编译
*预编译前奏:
i、imply global 暗示全局变量:即任何变量,如果变量未经声明就赋值,此变量就为全局对象所有
function test() { var a = b = 10; } test(); console.log(b); // 10 console.log(a); // a is not defined(…) // 此demo存在一个坑, 就是连续赋值 var a = b = 10; // 该赋值可以分解为两条语句,分别为b = 10; var a = b; // 所以b归全局对象所有,而a归test执行时的执行期上下文所有, //外部全局对象window访问不了test内部的a变量,并且test执行结束后a会随着test的执行期上下文销毁而消失
ii、一切声明的全局变量,全是window的属性。
var a = 10; console.log(window.a); // 10
(题外话:关于此处有关于对象的一个知识点,关联一下, delete操作符可以删除对象的属性,所以未经声明就赋值的变量为window对象的属性,可以用delete操作符删除,但在全局环境中用var声明的变量虽然可以用window.name访问,但无法用delete操作符删除)(可以这样理解,不用var声明直接为变量赋值的行为是为全局对象的属性赋值的操作,而用var声明的变量是真正的我们所说的变量,而不是某个对象的属性,当然该变量也是当前环境下this的属性)
*预编译四部曲:
i、创建AO(Activation Object)对象
ii、找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
iii、将实参值和形参统一
iiii、在函数体里面找函数声明,值赋予函数体
var a = 20; function test () { console.log(a); // 此处一个小坑 if(false) { var a = 10; } } test(); // 20 or undefined? answer is undefined // 预编译时与if判断的正误没有关系,只有在解释执行时才会在乎if判断的正误
(3)、解释执行
------------------------------end