Js高级程序设计第三版学习(七章)
第七章 函数表达式
定义函数有两种方式 函数声明, 和函数表达式
函数声明: function 后面跟标识符name, 函数声明提升: 代码流在执行时 会先读取函数声明,所以函数声明代码可以放在函数调用后面.
func();//我是函数声明
function func() {
console.log('我是函数声明');
}
函数表达式: 把匿名函数赋值给变量,通过变量调用, 匿名函数: function后面不跟标识符, 由于是赋值操作,所以不存在提升问题
//函数表达式
var func = function () {
console.log('函数表达式');
};
func();
命名函数表达式 : 如果我们仅仅想在函数内部调用自身,那么可以使用命名函数表达式, 命名函数表达式在函数外部是无法调用的
//命名函数表达式 只能在函数内部调用
var func = function fn() {
console.log('命名函数表达式');
fn();//会无限调用
};
func();
fn();//fn is not defined
立即执行函数(IIFE) : 将函数声明包在一个括号中'()',作为函数表达式,然后跟一个(参数)表示立即调用, 立即执行函数不会污染全局,因为函数执行后里面的作用域链就被销毁了, IIFE 有两种写法 (函数声明)(), (函数声明()),两种写法使用方法一样
//IIFE 立即执行函数
(function(name) {
var tomorrow = 'tomorrow';
console.log(name); //lmx
})('lmx');
console.log(tomorrow); //tomorrow is not defineds
立即执行函数前面可以放运算符(+ - * / % = ......), 代码会在函数执行完成后再执行运算操作
//IIFE 立即执行函数
var result = (function() {
var num = 5;
return num;
})() % (function(){
var num = 10
return num
})();
console.log(result); // 5
如果函数不是立即执行函数,也想调用自身的情况下,也可以用如下的形式, 因为赋值操作已经完成,所以不会报错,但如果是立即执行函数则会报错 func() not defined
//与上面相似
var func = function () {
console.log('函数表达式');
func();//会无限调用
};
func();
立即执行函数调用自身, 就需要使用命名表达式了
var func = (function fn() {
console.log('函数表达式');
fn();//会无限调用
})();
1.递归函数: : 就是函数调用自身,递归中使用命名函数表达式,使用函数声明的时候有可能会出现错误,下面对书中递归例子给行解释
//递归
function fn(num) {
console.log(num)
if (num < 1) {
return 1;
} else {
//通过函数名调用自身
return fn(num - 1); //改成nn()
}
}
//让nn等于这个函数指针
var nn = fn;
//让fn指向null
fn = null;
/*
此时会报错 fn is not a function
但是 此时函数会执行一次 console 然后才报错
一开始我理解为 应该直接报错 因为这时候fn = null
出现这种情况是因为 fn仅仅是一个标识符 用来存放指向这个函数体的指针
再赋值时fn把这个指针赋值给了nn 此时nn也指向了函数体,
改变fn的指向并没有改变nn, nn还是指针,指向函数体, 此时nn还是function,对于引用类型都适用
递归中使用函数名调用自己是存在风险的,所以建议使用命名函数表达式的形式
*/
console.log(nn(5)); //5
console.log(typeof nn)//function
2.闭包:
闭包实质就是一个函数可以访问另一个函数作用域中的变量,闭包最常见的方式就是在a函数内部在创建一b个函数,b函数的作用域有权访问a函数内部的变量, 当我们调用a方法并返回b方法, 然后再调用b函数时 我们依旧可以使用a函数内部的变量,因为此时b函数内部的变量对象包含着a函数的变量对象
//闭包
function wrapper(){
var xiaoMing = 'xiaoMing';
return function (){
console.log(xiaoMing)
}
}
var fn = wrapper();
fn();//xiaoMing
当第一次执行一个函数时,会创建一个执行环境,和相应的作用域链,把作用域链赋值给一个特殊的属性[[scope]],然后初始化这个函数的变量对象,这个函数的上下文的[[scope]]中 第一项 是这个函数本身的变量对象的指针 第二项是 外部函数的变量对象的指针, .... 最后一项 是全局的变量对象的指针, 所以某些函数可能公用一个函数的变量对象
//闭包 公用一个变量对象的例子
// 可以看到 public 被改变了两次
var public = 'xiaoHei';
function change1(){
public = 'chang1';
}
function change2(){
return function(){
public = 'return fn';
}
}
change1();
console.log(public)//chang1
var fn = change2();
fn();
console.log(public)//return fn
闭包会携带包含他的作用域,占用比普通函数更多的内存,请慎重使用
- 闭包与变量: 闭包只能取得包含函数中的最后一个值
result数组中的匿名函数的作用域链,指向的都是同一个包含函数handle的活动变量对象,在最后一次循环结束的时候index的值为5 所以此时result的所有匿名函数取得的index都为5
function handle() {
var result = [];
for (var index = 0; index < 5; index++) {
result[index] = function(){
console.log(index)
}
}
console.log(index);//5
return result;
}
var result = handle();
result[0]();//5
result[1]();//5
我们可以使用IIFE强制修改闭包的行为,来符合我们的预期, 此时result数组的匿名函数,都是通过IIFE返回的匿名函数,这些匿名函数都有一个各自的一个匿名的包含函数,无论是以传参的形式,保存index, 还是以声明变量的形式保存index,包含函数中都是符合我们预期的index值.
function handle() {
var result = [];
for (var index = 0; index < 5; index++) {
result[index] = (function(num) {
var hehe = index;
return function() {
console.log(hehe);
console.log(num);
};
})(index);
}
return result;
}
var result = handle();
result[0](); //00
result[1](); //11
- 关于this对象 : js的this对象,都是根据执行时的上下文来决定的, 如果我们在闭包中使用this 那么可能这个上下文对象并不一定是我们预期的上下文对象,而有可能是全局对象
var name = 'window'; var obj = { name:'lmx', handle:function(){ return function(){ console.log(this.name); } } } //实质上闭包的this.name 是window.name //它跟下面函数表达式的写法相似 调用者都是全局对象 obj.handle()()//window; var fn = obj.handle(); fn();//window //此时调用的才是obj的 name obj.newHandle = obj.handle(); obj.newHandle(); // lmx
我们可以在闭包的包含函数中定义 一个变量来存放上下文对象,来使闭包中的this符合我们的预期
var name = 'window';
var obj = {
name:'lmx',
handle:function(){
var that = this;
return function(){
console.log(that.name);
}
}
}
obj.handle()();//lmx
- 内存泄漏 : 在低版本ie中, 对于dom和bom对象可能使用的是引用计数的收回机制来处理内存,如果我们在闭包中使用了dom或者bom对象,虽然闭包已经执行完成,但是引用dom的变量并未被释放, 因为他的引用次数不为0,这时我们可能需要手动将变量设置为null,来释放内存
3.私有变量: 在任何函数定义的变量或函数,都可以被认为是私有变量,因为不能再函数外面使用私用变量,我们把有权访问私有变量或者私有函数的方法称之为特权方法
我们可以用对象的构造函数来创建特权方法, 此时实例拥有各自私有变量,而且每次创建一个实例,特权方法都会被新建(构造函数的弊端)
function Human(){
var tomorrow = ' tomorrow';
this.handle = function(){
console.log(tomorrow);
}
}
var li = new Human();
li.handle();//study.html:14 tomorrow
- 静态私有变量: 通过私有作用域创建的私有变量或方法, 其实就是原型模式, 通过原型的特权方法来访问私有变量,注意此时所有实例共享一个方法,而且每个实例并没有其独自的私有变量
function Person() {} (function() { var haha = 'haha'; Person.prototype.heihei = function() { console.log(haha); }; })(); var obj = new Person(); obj.heihei(); //haha
- 模块模式 : 其实就是为字面量方式创建的对象, 进行增强, 为其增加私有变量和特权方法
var single= (function(){ var name = 'lmx' return { name:'nihao', //特权方法 handle:function(){ console.log(name); } } })() console.log(single.name);//nihao single.handle();//lmx