笔记参考javascript.info中文站
函数对象,NFE
我们知道,函数是一种值,而且函数的类型是对象,因此,我们可以把函数看作一种 “行为对象” ,我们还可以对函数这个对象进行 “增/删属性”,“按引用传递” 等操作
1. 属性 “name”
函数的名字可以通过属性 “name” 来访问:
function sayHi() {
alert("Hi");
}
alert(sayHi.name); // sayHi
事实上,如果函数声明时没有显式的赋予函数名字,Javascript 引擎会自动根据上下文给函数命名:
function f(sayHi = function() {}) {
alert(sayHi.name); // sayHi(生效了!)
}
而且不可更改:
let sayHi = function() {
console.log('Hi');
};
sayHi.name = 'balabala';
console.log(sayHi.name); // sayHi(没变)
名字也可能是空字符串:
// 函数是在数组中创建的
let arr = [function() {}];
alert( arr[0].name ); // <空字符串>
2. 属性 “length”
内建属性 “length” 返回函数入参的个数:
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}
alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2,rest 参数不参与计数
这个属性常见于多态性函数中,库中的函数会根据函数被调用时传入参数的个数决定哪一套逻辑将被使用
3. 自定义属性
我们也可以添加我们自己的属性
举个例子:
function sayHi() {
alert("Hi");
// 计算调用次数
sayHi.counter++;
}
sayHi.counter = 0; // 初始值
sayHi(); // Hi
sayHi(); // Hi
alert( `Called ${sayHi.counter} times` ); // Called 2 times
我们现在可以利用函数的自定义属性将上一张讲的 “闭包” 修改一下:
function makeCounter() {
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
counter.count = 10;
alert( counter() ); // 10
现在我们的外部变量也可以随意修改内部的函数的变量了,如果我们不希望外部修改内部,那么还是闭包更合适
4. 命名函数表达式(NFE,Named Function Expression)
命名函数表达式指带有名字的函数表达式
我们将函数赋值给一个变量的时候往往不会给函数增加名字,即使加了名字,这段代码也只是函数表达式的复制过程,而非一个函数的正常声明过程
但是,NFE有其独特之处:函数内部可以调用函数本身,此调用对外部不可见
普通的函数声明也可以做到用函数本身这一点,而且对外部可见,但有时候我们就是需要函数表达式来做一些事,这时候我们并不能直接通过调用赋值的变量来调用函数本身,因为这个变量可能在函数外部被修改,从而导致整个函数从此以后不能使用,即使这个函数在外部变量修改之前已经被赋值给了其他变量:
let sayHi = function(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
sayHi("Guest"); // Error: sayHi is not a function
}
};
sayHi(); // Hello, Guest
func(); // Error, func is not defined(在函数外不可见)
let welcome = sayHi; // 赋值给了其他变量
sayHi = null; // 原变量被修改
welcome(); // Error,嵌套调用 sayHi 不再有效!
NFE 就没这个问题:
let sayHi = function func(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
func("Guest"); // 现在一切正常
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // Hello, Guest(嵌套调用有效)
“new Function” 语法
“new Function” 语法是一种创建函数的方法,不常使用
1. 语法
let func = new Function ([arg1, arg2, ...argN], functionBody);
举两个例子:
let sum = new Function('a', 'b', 'return a + b');
alert( sum(1, 2) ); // 3
let sayHi = new Function('alert("Hello")');
sayHi(); // Hello
请注意,参数和函数体部分都是字符串
这样的设定方便我们把从服务器中接收到的代码字符串直接使用在我们的函数中:
let str = ... 动态地接收来自服务器的代码 ...
let func = new Function(str);
func();
2. 闭包
闭包是指使用一个特殊的属性 [[Environment]]
来记录函数自身的创建时的环境的函数,指向函数创建之初的词法环境
但是使用 “new Function” 语法创建的函数的 [[Environment]]
指向全局环境
因此,此类函数只能访问全局变量
function getFunc() {
let value = "test";
let func = new Function('alert(value)');
return func;
}
getFunc()(); // error: value is not defined
“new Function” 语法创建的函数是从服务器中直接接受来的,因此我们在编写代码时并不知道我们将接收到什么样的代码,不可控性非常高,因此我们要尽可能避免服务器代码对内部函数以及变量进行修改
setTimeout 和 setInterval
- setTimeout 允许我们将函数推迟到一段时间间隔之后再执行。
- setInterval 允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。
1. setTimeout
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
func|code
指希望延迟的代码或函数,如果传入的是字符串,JavaScript 会自动为其创建一个函数:
setTimeout("alert('Hello')", 1000);
// 我们也可以使用箭头函数代替它们
setTimeout(() => alert('Hello'), 1000);
delay
指延迟的时间,毫秒单位
后面的 arg1......
指被延迟的函数的参数,由函数来决定
举个例子:
function sayHi(phrase, who) {
alert( phrase + ', ' + who );
}
setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John
setTimeout
在调用时会返回一个“定时器标识符(timer identifier)”,我们可以使用 clearTimeout()
来取消这个调度,但是标识符不会因此而改变:
let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // 定时器标识符
clearTimeout(timerId); // 取消调度,导致什么都没有发生
alert(timerId); // 还是这个标识符(并没有因为调度被取消了而变成 null)
2. setInterval
setInterval
方法和 setTimeout
的语法相同,所有参数的意义也相同
唯一的区别在于,setInterval
方法会每隔 delay
毫秒就重新执行一次函数:
// 每 2 秒重复一次
let timerId = setInterval(() => alert('tick'), 2000);
// 5 秒之后停止
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
为了避免 setTimeout
和 setInterval
在等待过程中函数没有引用而导致被垃圾回收系统清理, setTimeout
和 setInterval
中的函数会被创建一个内部引用,这样就不会被误删了。
3. 嵌套的 setTimeout
我们也可是采用嵌套的 setTimeout
来实现周期调度;
let timerId = setTimeout(function tick() {
alert('tick');
timerId = setTimeout(tick, 2000); // (*)
}, 2000);
这样做比 setInterval
函数更加灵活,因为变量的值可以继承,不会重新初始化
我们甚至可以需修改每次 setTimeout
的参数,比如根据情况动态修改延迟时间
除此之外还有一个硬件问题:
setInterval
函数的延迟时间实际上是吧函数执行时间也包含在内的,因此实际等待的时间比 delay
设置的要短一点点
但是嵌套的 setTimeout
没有这个问题,一定是等函数运行结束再进行 delay
时间的等待
4. 零延时的 setTimeout
用 setTimeout(func, 0)
,或者仅仅是 setTimeout(func)
可以使用零延时的 setTimeout
这种写法可以让我们在整个脚本运行完之后的瞬间在运行某段代码:
setTimeout(() => alert("World"));
alert("Hello");
// 最终结果是“Hello”、“World”
实际上零延迟并不是真的零延迟,在 5 重嵌套定时器之后,时间间隔被强制设定为至少 4 毫秒