下述内容主要讲述了《JavaScript高级程序设计(第3版)》第7章关于“函数表达式”。
一、回顾
定义函数的方式有两种:第一种是“函数声明”,另一种就是“函数表达式”。
“函数声明”会被提升,意味着把函数声明放在调用它的语句后面。
示例1:
a(); // a
b(); // TypeError: b is not a function
function a() {
console.log("a");
}
var b = function() {
console.log("b");
};
声明本身会被提升,而包含函数表达式在内的赋值并不会被提升。
函数提升的关键,就是理解函数声明与函数表达式之间的区别。
了解更多的变量提升问题,请查看JavaScript提升(你不知道的JavaScript)
示例2:
if(true) {
function sayHi() {
console.log("Hi, Jerry!")
}
} else {
function sayHi() {
console.log("Hi, Tang!");
}
}
sayHi();
// 在chrome、firefox下输出:Hi, Jerry!
// 在Safari下输出:Hi, Tang!
示例3:
var sayHi;
if(true) {
sayHi = function() {
console.log("Hi, Jerry!")
}
} else {
sayHi = function() {
console.log("Hi, Tang!");
}
}
sayHi();
// 全部输出:Hi, Jerry!
示例4:
function sayHi() {
console.log("Hi, Jerry!")
}
function sayHi() {
console.log("Hi, Tang!");
}
sayHi(); // 全部输出:Hi, Tang!
说明:后面的函数声明可以覆盖前面的。
二、递归
示例5:
function factorial(num) {
if(num <= 1) {
return 1; // 书写递归函数,尽量要先写结束条件
} else {
return num * factorial(num-1); // num--
}
}
factorial(4); // 24
var anotherFactorial = factorial;
factorial = null;
anotherFactorial(3); // TypeError: factorial is not a function
原因:在调用anotherFactorial()时,由于必须执行factorial(),而factorial已经不再是函数,所以就会导致错误。
function factorial(num) {
if(num <= 1) {
return 1;
} else {
return num * arguments.callee(num-1); // num--
}
}
注意:在严格模式下,不允许使用arguments.callee
示例6 – 具名函数:
var factorial = function fn(num) {
if(num <= 1) {
return 1;
} else {
return num * fn(num-1); // num--
}
}
var anotherFactorial = factorial;
factorial = null;
anotherFactorial(3); // 6
三、闭包
形式:在一个函数内部创建另外一个函数。
定义:指有权访问另一个函数作用域中的变量的函数。
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
JavaScript作用域闭包(你不知道的JavaScript)
示例7:
function createComparisonFunction(propertyName) {
return function(obj1, obj2) {
var value1 = obj1[propertyName],
value2 = obj2[propertyName];
return value1 - value2;
}
}
var p1 = { age: 25 };
var p2 = { age: 26 };
var compareAge = createComparisonFunction("age");
var result = compareAge(p1, p2);
if(result === 0) {
console.log("一样大!")
}else if(result > 0) {
console.log("p1大!")
}else {
console.log("p2大!");
}
compareAge = null; // 释放内存
作用:一般来讲,函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。
上述createComparisonFunction执行完毕,其作用域会被销毁,但其活动对象仍然保存在内存中,直到匿名函数被销毁后。
解释:每个执行环境都有一个表示变量的对象–变量对象。作用域链本质是一个指向变量对象的指针列表,它只引用但不实际包含变量对象!!!
建议:过多的闭包可能会导致内存占用过多,建议只在绝对必要时使用闭包。
示例8–经典面试题(为每个li绑定click事件,并输入对应的顺序号。):
<ul>
<li>第一条</li>
<li>第二条</li>
<li>第三条</li>
<li>第四条</li>
</ul>
每个函数的作用域中都保存着活动对象,所以它们引用的都是一个变量i。
var list = document.getElementsByTagName("li");
for(var i = 0, len = list.length; i < len; i++) {
list[i].onclick = function() {
console.log(i); // 4
}
}
通过创建另一个匿名函数强制让闭包的行为符合预期。
for(var i = 0, len = list.length; i < len; i++) {
list[i].addEventListener("click", (function(i) {
return function() { // function(i)
console.log(i);
}
})(i))
}
更巧妙的方法
list.onclick = function() {
console.log($(this).prevAll().length);
}
四、this对象
this对象在运行时基于函数的执行环境绑定。
在全局函数中,this等于window,当函数作为某个对象的方法调用时,this等于当前对象。
JavaScript中的this(你不知道的JavaScript)
示例9:
var name = "window";
var object = {
name: "current object",
getName: function() {
return this.name;
}
};
object.getName(); // current object
var name = "window";
var object = {
name: "current object",
getName: function() {
return function() {
return this.name;
}
}
};
object.getName()(); // window
// var getNameByWindow = object.getName();
// getNameByWindow();
var name = "window";
var object = {
name: "current object",
getName: function() {
var that = this;
return function() {
return that.name;
}
}
};
object.getName()(); // current object
五、内存溢出
JavaScript对象和Dom对象循环引用,导致内存不能释放,内存溢出!
示例10:
var element = document.getElementById("id"); // Dom对象
var id = element.id; // JavaScript对象
element.onclick = function() {
console.log(id);
};
element = null;
必须记住:闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。因此有必要把element设置为null,解除对DOM对象的引用,顺利减少其引用数,收回占用内存。
六、块级作用域
JavaScript词法作用域(你不知道的JavaScript)
示例11:
for(var i = 0; i < 10; i++) {}
var i; // 被忽略,变量提示
console.log(i); // 10
(function() {
for(var i = 0; i < 10; i++) {}
})();
var i;
console.log(i); // undefined
七、私有变量
JavaScript函数作用域,使得函数中定义的变量,都可以被认为是私有变量。
示例12 –构造函数模式:
function Person(name) {
this.getName = function() {
return name;
};
}
var p1 = new Person("Jerry");
var p2 = new Person("Tang");
p1.getName();
p2.getName();
每个实例都会创建上述同样的方法
示例13 – 静态私有变量:
(function() {
var name = "";
Person = function(value) {
name = value;
};
Person.prototype.getName = function() {
return name;
}
})();
var p1 = new Person("Jerry");
var p2 = new Person("Tang");
p1.getName();
p2.getName();
代码得到了复用,但是所有实例返回相同值
模块模式:创建的每个单例都是Object的实例。
var getSingle = function(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments));
};
};
// 测试
function testSingle(){}
getSingle(testSingle)() === getSingle(testSingle)(); // true
所以,要视情况而定,选择何种方式创建私有变量!