全局对象
概述
- 全局对象提供可在任何地方使用的变量和函数
- 默认情况下,这些全局变量内建于语言或环境中
- 在浏览器中,它的名字是 “
window
”,对Node.js
而言,它的名字是 “global
”,其它环境可能用的是别的名字 - 最近,
globalThis
被作为全局对象的标准名称加入到了JavaScript
中,所有环境都应该支持该名称
使用
- 全局对象的所有属性都可以被直接访问:
1 2 3 |
alert("Hello"); // 等同于 window.alert("Hello"); |
- 在浏览器中,使用
var
(而不是let/const
!)声明的全局函数和变量会成为全局对象的属性- 函数声明(特指在主代码流中具有
function
关键字的语句,而不是函数表达式)也有这样的效果 - 这种行为是出于兼容性而存在的
- 函数声明(特指在主代码流中具有
1 2 3 |
var gVar = 5; alert(window.gVar); // 5(成为了全局对象的属性) |
- 如果一个值非常重要,以至于你想使它在全局范围内可用,那么可以直接将其作为属性写入:
- 也就是说,一般不建议使用全局变量
1 2 3 4 5 6 7 8 9 10 11 |
// 将当前用户信息全局化,以允许所有脚本访问它 window.currentUser = { name: "John" }; // 代码中的另一个位置 alert(currentUser.name); // John // 或者,如果我们有一个名为 "currentUser" 的局部变量 // 从 window 显式地获取它(这是安全的!) alert(window.currentUser.name); // John |
使用 polyfills
- 我们使用全局对象来测试对现代语言功能的支持
- 例如,测试是否存在内建的
Promise
对象(在版本特别旧的浏览器中不存在):
- 例如,测试是否存在内建的
1 2 3 |
if (!window.Promise) { alert("Your browser is really old!"); } |
- 如果没有(例如,我们使用的是旧版浏览器),那么我们可以创建 “
polyfills
”:添加环境不支持但在现代标准中存在的功能
1 2 3 |
if (!window.Promise) { window.Promise = ... // 定制实现现代语言功能 } |
函数对象,NFE
概述
- 已经知道,在
JavaScript
中,函数也是一个值- 而
JavaScript
中的每个值都有一种类型,那么函数是什么类型呢? - 在
JavaScript
中,函数的类型是对象
- 而
- 一个容易理解的方式是把函数想象成可被调用的”行为对象(
action object
)“- 我们不仅可以调用它们,还能把它们当作对象来处理:增/删属性,按引用传递等
属性name
- 函数对象包含一些便于使用的属性
- 比如,一个函数的名字可以通过属性 “
name
” 来访问:
- 比如,一个函数的名字可以通过属性 “
1 2 3 4 5 6 |
function sayHi() { alert("Hi"); } alert(sayHi.name); // sayHi |
- 更有趣的是,名称赋值的逻辑很智能。即使函数被创建时没有名字,名称赋值的逻辑也能给它赋予一个正确的名字,然后进行赋值:
1 2 3 4 5 |
let sayHi = function() { alert("Hi"); }; alert(sayHi.name); // sayHi(有名字!) |
- 当以默认值的方式完成了赋值时,它也有效:
- 规范中把这种特性叫做「上下文命名」
- 如果函数自己没有提供,那么在赋值中,会根据上下文来推测一个
1 2 3 4 5 |
function f(sayHi = function() {}) { alert(sayHi.name); // sayHi(生效了!) } f(); |
- 对象方法也有名字:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let user = { sayHi() { // ... }, sayBye: function() { // ... } } alert(user.sayHi.name); // sayHi alert(user.sayBye.name); // sayBye |
- 有时会出现无法推测名字的情况。此时,属性
name
会是空,像这样:
1 2 3 4 5 |
// 函数是在数组中创建的 let arr = [function() {}]; alert( arr[0].name ); // <空字符串> // 引擎无法设置正确的名字,所以没有值 |
属性length
- 还有另一个内建属性 “
length
”,它返回函数入参的个数,比如:- 可以看到,
rest
参数不参与计数
- 可以看到,
1 2 3 4 5 6 7 |
function f1(a) {} function f2(a, b) {} function many(a, b, ...more) {} alert(f1.length); // 1 alert(f2.length); // 2 alert(many.length); // 2 |
- 属性
length
有时在操作其它函数的函数中用于做 内省/运行时检查(introspection
)- 比如,下面的代码中函数
ask
接受一个询问答案的参数question
和可能包含任意数量handler
的参数...handlers
- 当用户提供了自己的答案后,函数会调用那些
handlers
。我们可以传入两种handlers
:
一种是无参函数,它仅在用户给出肯定回答时被调用
一种是有参函数,它在两种情况都会被调用,并且返回一个答案 - 为了正确地调用
handler
,我们需要检查handler.length
属性
- 比如,下面的代码中函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function ask(question, ...handlers) { let isYes = confirm(question); for(let handler of handlers) { if (handler.length == 0) { if (isYes) handler(); } else { handler(isYes); } } } // 对于肯定的回答,两个 handler 都会被调用 // 对于否定的回答,只有第二个 handler 被调用 ask("Question?", () => alert('You said yes'), result => alert(result)); |
自定义属性
- 也可以添加我们自己的属性
- 这里我们添加了
counter
属性,用来跟踪总的调用次数:
- 这里我们添加了
1 2 3 4 5 6 7 8 9 10 11 12 |
function sayHi() { alert("Hi"); // 计算调用次数 sayHi.counter++; } sayHi.counter = 0; // 初始值 sayHi(); // Hi sayHi(); // Hi alert( `Called ${sayHi.counter} times` ); // Called 2 times |
- 属性不是变量
- 被赋值给函数的属性,比如
sayHi.counter = 0
,不会 在函数内定义一个局部变量counter
- 换句话说,属性
counter
和变量let counter
是毫不相关的两个东西 - 我们可以把函数当作对象,在它里面存储属性,但是这对它的执行没有任何影响。变量不是函数属性,反之亦然
- 被赋值给函数的属性,比如
- 函数属性有时会用来替代闭包
- 现在
count
被直接存储在函数里,而不是它外部的词法环境 - 那么它和闭包谁好谁赖?
- 两者最大的不同就是如果
count
的值位于外层(函数)变量中,那么外部的代码无法访问到它,只有嵌套的那些函数可以修改它
- 现在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function makeCounter() { // 不需要这个了 // let count = 0 function counter() { return counter.count++; }; counter.count = 0; return counter; } let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 而如果它是绑定到函数的,那么就可以这样: function makeCounter() { function counter() { return counter.count++; }; counter.count = 0; return counter; } let counter = makeCounter(); counter.count = 10; alert( counter() ); // 10 |
命名函数表达式
- 命名函数表达式(
NFE
,Named Function Expression
),指带有名字的函数表达式的术语 - 例如,让我们写一个普通的函数表达式:
- 然后给它加一个名字:
- 为它添加一个
"func"
名字的目的是什么? - 首先请注意,它仍然是一个函数表达式
在function
后面加一个名字"func"
没有使它成为一个函数声明,因为它仍然是作为赋值表达式中的一部分被创建的
1 2 3 |
let sayHi = function(who) { alert(`Hello, ${who}`); }; |
1 2 3 |
let sayHi = function func(who) { alert(`Hello, ${who}`); }; |
- 添加这个名字当然也没有打破任何东西,函数依然可以通过
sayHi()
来调用:
1 2 3 4 5 |
let sayHi = function func(who) { alert(`Hello, ${who}`); }; sayHi("John"); // Hello, John |
- 关于名字
func
有两个特殊的地方,这就是添加它的原因:- 它允许函数在内部引用自己
- 它在函数外是不可见的
- 例如,下面的函数
sayHi
会在没有入参who
时,以"Guest"
为入参调用自己:
1 2 3 4 5 6 7 8 9 10 11 12 |
let sayHi = function func(who) { if (who) { alert(`Hello, ${who}`); } else { func("Guest"); // 使用 func 再次调用函数自身 } }; sayHi(); // Hello, Guest // 但这不工作: func(); // Error, func is not defined(在函数外不可见) |
- 为什么使用
func
呢?为什么不直接使用sayHi
进行嵌套调用?- 大多数情况下我们可以像下面
1
这样写: - 这段代码的问题在于
sayHi
的值可能会被函数外部的代码改变,如2
如果该函数被赋值给另外一个变量(译注:也就是原变量被修改),那么函数就会开始报错:
- 大多数情况下我们可以像下面
1 2 3 4 5 6 7 8 9 |
// 1 let sayHi = function(who) { if (who) { alert(`Hello, ${who}`); } else { sayHi("Guest"); } }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 2 let sayHi = function(who) { if (who) { alert(`Hello, ${who}`); } else { sayHi("Guest"); // Error: sayHi is not a function } }; let welcome = sayHi; sayHi = null; welcome(); // Error,嵌套调用 sayHi 不再有效! |
-
为什么会发生上面的情况:
- 发生这种情况是因为该函数从它的外部词法环境获取
sayHi
。 - 没有局部的
sayHi
了,所以使用外部变量。而当调用时,外部的sayHi
是null
。
- 发生这种情况是因为该函数从它的外部词法环境获取
-
给函数表达式添加的可选的名字,正是用来解决这类问题的
- 现在它可以正常运行了,因为名字
func
是函数局部域的 - 它不是从外部获取的(而且它对外部也是不可见的)
- 现在它可以正常运行了,因为名字
1 2 3 4 5 6 7 8 9 10 11 12 |
let sayHi = function func(who) { if (who) { alert(`Hello, ${who}`); } else { func("Guest"); // 现在一切正常 } }; let welcome = sayHi; sayHi = null; welcome(); // Hello, Guest(嵌套调用有效) |
new Function
语法
语法
- 该函数是通过使用参数
arg1...argN
和给定的functionBody
创建的
1 |
let func = new Function ([arg1, arg2, ...argN], functionBody); |
- 示例
1
- 一个带有两个参数的函数:
1 2 3 |
let sum = new Function('a', 'b', 'return a + b'); alert( sum(1, 2) ); // 3 |
- 示例
2
- 一个没有参数的函数,只有函数体:
1 2 3 |
let sayHi = new Function('alert("Hello")'); sayHi(); // Hello |
对比
- 与我们已知的其他方法相比,这种方法最大的不同在于,它实际上是通过运行时通过参数传递过来的字符串创建的
- 以前的所有声明方法都需要我们 —— 程序员,在脚本中编写函数的代码
- 但是
new Function
允许我们将任意字符串变为函数- 例如,我们可以从服务器接收一个新的函数并执行它:
1 2 3 4 |
let str = ... 动态地接收来自服务器的代码 ... let func = new Function(str); func(); |
闭包
- 通常,闭包是指使用一个特殊的属性
[[Environment]]
来记录函数自身的创建时的环境的函数- 它具体指向了函数创建时的词法环境
- 但是如果我们使用
new Function
创建一个函数,那么该函数的[[Environment]]
并不指向当前的词法环境,而是指向全局环境- 因此,此类函数无法访问外部(
outer
)变量,只能访问全局变量
- 因此,此类函数无法访问外部(
1 2 3 4 5 6 7 8 9 |
function getFunc() { let value = "test"; let func = new Function('alert(value)'); return func; } getFunc()(); // error: value is not defined |
1 2 3 4 5 6 7 8 9 |
function getFunc() { let value = "test"; let func = function() { alert(value); }; return func; } getFunc()(); // "test",从 getFunc 的词法环境中获取的 |
- 如果这个函数能够访问外部(
outer
)变量会怎么样?- 问题在于,在将 JavaScript 发布到生产环境之前,需要使用 压缩程序(
minifier
) 对其进行压缩
一个特殊的程序,通过删除多余的注释和空格等压缩代码 —— 更重要的是,将局部变量命名为较短的变量 - 例如,如果一个函数有
let userName
,压缩程序会把它替换为let a
(如果a
已被占用了,那就使用其他字符),剩余的局部变量也会被进行类似的替换 - 一般来说这样的替换是安全的,毕竟这些变量是函数内的局部变量,函数外的任何东西都无法访问它
- 在函数内部,压缩程序会替换所有使用了这些变量的代码
压缩程序很聪明,它会分析代码的结构,因此它不会“破坏”你的程序 - 但是在这种情况下,如果使
new Function
可以访问自身函数以外的变量,它也很有可能无法找到重命名的userName
,这是因为新函数的创建发生在代码压缩以后,变量名已经被替换了 - 所以,即使我们可以在
new Function
中访问外部词法环境,我们也会受挫于压缩程序
- 问题在于,在将 JavaScript 发布到生产环境之前,需要使用 压缩程序(
setTimeout
和 setInterval
概述
- 有时我们并不想立即执行一个函数,而是等待特定一段时间之后再执行
- 这就是所谓的“计划调用(
scheduling a call
)”
- 这就是所谓的“计划调用(
- 目前有两种方式可以实现:
setTimeout
允许我们将函数推迟到一段时间间隔之后再执行setInterval
允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数
- 这两个方法并不在
JavaScript
的规范中- 但是大多数运行环境都有内建的调度程序,并且提供了这些方法
- 目前来讲,所有浏览器以及
Node.js
都支持这两个方法
setTimeout
- 语法
func|code
想要执行的函数或代码字符串
一般传入的都是函数。由于某些历史原因,支持传入代码字符串,但是不建议这样做delay
执行前的延时,以毫秒为单位(1000
毫秒 =1
秒),默认值是0
arg1
,arg2…
要传入被执行函数(或代码字符串)的参数列表(IE9
以下不支持,见2
)
1 |
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...) |
1 2 3 4 5 |
function sayHi() { alert('Hello'); } setTimeout(sayHi, 1000); |
1 2 3 4 5 |
function sayHi(phrase, who) { alert( phrase + ', ' + who ); } setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John |
- 如果第一个参数位传入的是字符串,JavaScript 会自动为其创建一个函数
- 但是,不建议使用字符串,我们可以使用箭头函数代替它们,如下所示:
1 |
setTimeout("alert('Hello')", 1000); |
1 |
setTimeout(() => alert('Hello'), 1000); |
注意1
- 新手开发者有时候会误将一对括号
()
加在函数后面:- 这样不行,因为
setTimeout
期望得到一个对函数的引用 - 而这里的
sayHi()
很明显是在执行函数,所以实际上传入setTimeout
的是 函数的执行结果 - 在这个例子中,
sayHi()
的执行结果是undefined
(也就是说函数没有返回任何结果),所以实际上什么也没有调度
- 这样不行,因为
1 2 |
// 错的! setTimeout(sayHi(), 1000); |
clearTimeout
取消调度
setTimeout
在调用时会返回一个“定时器标识符(timer identifier
)”- 在我们的例子中是
timerId
,我们可以使用它来取消执行
- 在我们的例子中是
1 2 |
let timerId = setTimeout(...); clearTimeout(timerId); |
- 下面的代码中,我们对一个函数进行了调度,紧接着取消了这次调度(中途反悔了)
1 2 3 4 5 |
let timerId = setTimeout(() => alert("never happens"), 1000); alert(timerId); // 定时器标识符 clearTimeout(timerId); alert(timerId); // 还是这个标识符(并没有因为调度被取消了而变成 null) |
- 关于定时器标识符
- 从
alert
的输出来看,在浏览器中,定时器标识符是一个数字 - 在其他环境中,可能是其他的东西。例如
Node.js
返回的是一个定时器对象,这个对象包含一系列方法 - 重申一遍,这些方法没有统一的规范定义,所以这没什么问题
- 从
setInterval
setInterval
方法和setTimeout
的语法相同:- 所有参数的意义也是相同的
- 不过与
setTimeout
只执行一次不同,setInterval
是每间隔给定的时间周期性执行
1 |
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...) |
- 想要阻止后续调用,我们需要调用
clearInterval(timerId)
- 示例:将每间隔
2
秒就会输出一条消息。5
秒之后,输出停止:
1 2 3 4 5 |
// 每 2 秒重复一次 let timerId = setInterval(() => alert('tick'), 2000); // 5 秒之后停止 setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000); |
注意2
alert
弹窗显示的时候计时器依然在进行计时- 在大多数浏览器中,包括
Chrome
和Firefox
,在显示alert/confirm/prompt
弹窗时,内部的定时器仍旧会继续“嘀嗒” - 所以,在运行上面的代码时,如果在一定时间内没有关掉
alert
弹窗,那么在你关闭弹窗后,下一个alert
会立即显示。两次alert
之间的时间间隔将小于2
秒
- 在大多数浏览器中,包括
嵌套的 setTimeout
- 周期性调度有两种方式
- 一种是使用
setInterval
- 另外一种就是嵌套的
setTimeout
- 一种是使用
- 示例
- 这个
setTimeout
在当前这一次函数执行完时(*)
立即调度下一次调用
- 这个
1 2 3 4 5 6 7 8 |
/** instead of: let timerId = setInterval(() => alert('tick'), 2000); */ let timerId = setTimeout(function tick() { alert('tick'); timerId = setTimeout(tick, 2000); // (*) }, 2000); |
- 嵌套的
setTimeout
要比setInterval
灵活得多,采用这种方式可以根据当前执行结果来调度下一次调用,因此下一次调用可以与当前这一次不同- 例如,我们要实现一个服务(
server
),每间隔5
秒向服务器发送一个数据请求,但如果服务器过载了,那么就要降低请求频率,比如将间隔增加到10
、20
、40
秒等 - 并且,如果我们调度的函数占用大量的
CPU
,那么我们可以测量执行所需要花费的时间,并安排下次调用是应该提前还是推迟
- 例如,我们要实现一个服务(
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 伪代码: let delay = 5000; let timerId = setTimeout(function request() { ...发送请求... if (request failed due to server overload) { // 下一次执行的间隔是当前的 2 倍 delay *= 2; } timerId = setTimeout(request, delay); }, delay); |
- 嵌套的
setTimeout
相较于setInterval
能够更精确地设置两次执行之间的延时- 使用
setInterval
时,func
函数的实际调用间隔要比代码中设定的时间间隔要短! - 这也是正常的,因为
func
的执行所花费的时间“消耗”了一部分间隔时间 - 也可能出现这种情况,就是
func
的执行所花费的时间比我们预期的时间更长,并且超出了100
毫秒 - 在这种情况下,
JavaScript
引擎会等待func
执行完成,然后检查调度程序,如果时间到了,则 立即 再次执行它
极端情况下,如果函数每次执行时间都超过delay
设置的时间,那么每次调用之间将完全没有停顿 - 而嵌套的
setTimeout
就能确保延时的固定(这里是100
毫秒)
- 使用
1 2 3 4 |
let i = 1; setInterval(function() { func(i++); }, 100); |
1 2 3 4 5 |
let i = 1; setTimeout(function run() { func(i++); setTimeout(run, 100); }, 100); |
注意3
- 垃圾回收和
setInterval/setTimeout
回调(callback
)- 当一个函数传入
setInterval/setTimeout
时,将为其创建一个内部引用,并保存在调度程序中 - 这样,即使这个函数没有其他引用,也能防止垃圾回收器(
GC
)将其回收 - 对于
setInterval
,传入的函数也是一直存在于内存中,直到clearInterval
被调用
- 当一个函数传入
1 2 |
// 在调度程序调用这个函数之前,这个函数将一直存在于内存中 setTimeout(function() {...}, 100); |
零延时的 setTimeout
- 有一种特殊的用法:
setTimeout(func, 0)
,或者仅仅是setTimeout(func)
- 这样调度可以让
func
尽快执行
- 这样调度可以让
- 但是只有在当前正在执行的脚本执行完成后,调度程序才会调用它
- 也就是说,该函数被调度在当前脚本执行完成“之后”立即执行
- 第一行代码“将调用安排到日程(
calendar
)0
毫秒处” - 但是调度程序只有在当前脚本执行完毕时才会去“检查日程”,所以先输出
"Hello"
,然后才输出"World"
1 2 3 |
setTimeout(() => alert("World")); alert("Hello"); |
注意4
- 零延时实际上不为零(在浏览器中)
- 在浏览器环境下,嵌套定时器的运行频率是受限制的
- 根据 HTML5 标准 所讲:“经过
5
重嵌套定时器之后,时间间隔被强制设定为至少4
毫秒”
- 示例
- 第一次,定时器是立即执行的(正如规范里所描述的那样)
timer
数组里存放的是每次定时器运行的时刻与start
的差值,所以数字只会越来越大,实际上前后调用的延时是数组值的差值(示例中前几次都是1
,所以延时为0
)- 如果我们使用
setInterval
而不是setTimeout
,也会发生类似的情况:setInterval(f)
会以零延时运行几次f
,然后以4
毫秒以上的强制延时运行 - 这个限制来自“远古时代”,并且许多脚本都依赖于此,所以这个机制也就存在至今
1 2 3 4 5 6 7 8 9 10 11 12 |
let start = Date.now(); let times = []; setTimeout(function run() { times.push(Date.now() - start); // 保存前一个调用的延时 if (start + 100 < Date.now()) alert(times); // 100 毫秒之后,显示延时信息 else setTimeout(run); // 否则重新调度 }); // 输出示例: // 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100 |
- 对于服务端的
JavaScript
,就没有这个限制,并且还有其他调度即时异步任务的方式- 例如
Node.js
的setImmediate
- 因此,这个提醒只是针对浏览器环境的
- 例如
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!