• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-04-06 13:57 Aet 隐藏边栏 |   抢沙发  2 
文章评分 2 次,平均分 5.0

全局对象

概述

  1. 全局对象提供可在任何地方使用的变量和函数
    1. 默认情况下,这些全局变量内建于语言或环境中
  2. 在浏览器中,它的名字是 “window”,对 Node.js 而言,它的名字是 “global”,其它环境可能用的是别的名字
  3. 最近,globalThis 被作为全局对象的标准名称加入到了 JavaScript 中,所有环境都应该支持该名称

使用

  1. 全局对象的所有属性都可以被直接访问:

  1. 在浏览器中,使用 var(而不是 let/const!)声明的全局函数和变量会成为全局对象的属性
    1. 函数声明(特指在主代码流中具有 function 关键字的语句,而不是函数表达式)也有这样的效果
    2. 这种行为是出于兼容性而存在的

  1. 如果一个值非常重要,以至于你想使它在全局范围内可用,那么可以直接将其作为属性写入:
    1. 也就是说,一般不建议使用全局变量

使用 polyfills

  1. 我们使用全局对象来测试对现代语言功能的支持
    1. 例如,测试是否存在内建的 Promise 对象(在版本特别旧的浏览器中不存在):

  1. 如果没有(例如,我们使用的是旧版浏览器),那么我们可以创建 “polyfills”:添加环境不支持但在现代标准中存在的功能

函数对象,NFE

概述

  1. 已经知道,在 JavaScript 中,函数也是一个值
    1. JavaScript 中的每个值都有一种类型,那么函数是什么类型呢?
    2. JavaScript 中,函数的类型是对象
  2. 一个容易理解的方式是把函数想象成可被调用的”行为对象(action object)“
    1. 我们不仅可以调用它们,还能把它们当作对象来处理:增/删属性,按引用传递等

属性name

  1. 函数对象包含一些便于使用的属性
    1. 比如,一个函数的名字可以通过属性 “name” 来访问:

  1. 更有趣的是,名称赋值的逻辑很智能。即使函数被创建时没有名字,名称赋值的逻辑也能给它赋予一个正确的名字,然后进行赋值:

  1. 当以默认值的方式完成了赋值时,它也有效:
    1. 规范中把这种特性叫做「上下文命名」
    2. 如果函数自己没有提供,那么在赋值中,会根据上下文来推测一个

  1. 对象方法也有名字:

  1. 有时会出现无法推测名字的情况。此时,属性 name 会是空,像这样:

属性length

  1. 还有另一个内建属性 “length”,它返回函数入参的个数,比如:
    1. 可以看到,rest 参数不参与计数

  1. 属性 length 有时在操作其它函数的函数中用于做 内省/运行时检查(introspection
    1. 比如,下面的代码中函数 ask 接受一个询问答案的参数 question 和可能包含任意数量 handler 的参数 ...handlers
    2. 当用户提供了自己的答案后,函数会调用那些 handlers。我们可以传入两种 handlers
      一种是无参函数,它仅在用户给出肯定回答时被调用
      一种是有参函数,它在两种情况都会被调用,并且返回一个答案
    3. 为了正确地调用 handler,我们需要检查 handler.length 属性

自定义属性

  1. 也可以添加我们自己的属性
    1. 这里我们添加了 counter 属性,用来跟踪总的调用次数:

  1. 属性不是变量
    1. 被赋值给函数的属性,比如 sayHi.counter = 0,不会 在函数内定义一个局部变量 counter
    2. 换句话说,属性 counter 和变量 let counter 是毫不相关的两个东西
    3. 我们可以把函数当作对象,在它里面存储属性,但是这对它的执行没有任何影响。变量不是函数属性,反之亦然
  2. 函数属性有时会用来替代闭包
    1. 现在 count 被直接存储在函数里,而不是它外部的词法环境
    2. 那么它和闭包谁好谁赖?
    3. 两者最大的不同就是如果 count 的值位于外层(函数)变量中,那么外部的代码无法访问到它,只有嵌套的那些函数可以修改它

命名函数表达式

  1. 命名函数表达式(NFENamed Function Expression),指带有名字的函数表达式的术语
  2. 例如,让我们写一个普通的函数表达式:
    1. 然后给它加一个名字:
    2. 为它添加一个 "func" 名字的目的是什么?
    3. 首先请注意,它仍然是一个函数表达式
      function 后面加一个名字 "func" 没有使它成为一个函数声明,因为它仍然是作为赋值表达式中的一部分被创建的

  1. 添加这个名字当然也没有打破任何东西,函数依然可以通过 sayHi() 来调用:

  1. 关于名字 func 有两个特殊的地方,这就是添加它的原因:
    1. 它允许函数在内部引用自己
    2. 它在函数外是不可见的
    3. 例如,下面的函数 sayHi 会在没有入参 who 时,以 "Guest" 为入参调用自己:

  1. 为什么使用 func 呢?为什么不直接使用 sayHi 进行嵌套调用?
    1. 大多数情况下我们可以像下面1这样写:
    2. 这段代码的问题在于 sayHi 的值可能会被函数外部的代码改变,如2
      如果该函数被赋值给另外一个变量(译注:也就是原变量被修改),那么函数就会开始报错:

  1. 为什么会发生上面的情况:

    1. 发生这种情况是因为该函数从它的外部词法环境获取 sayHi
    2. 没有局部的 sayHi 了,所以使用外部变量。而当调用时,外部的 sayHinull
  2. 给函数表达式添加的可选的名字,正是用来解决这类问题的

    1. 现在它可以正常运行了,因为名字 func 是函数局部域的
    2. 它不是从外部获取的(而且它对外部也是不可见的)

new Function 语法

语法

  1. 该函数是通过使用参数 arg1...argN 和给定的 functionBody 创建的

  1. 示例1
    1. 一个带有两个参数的函数:

  1. 示例2
    1. 一个没有参数的函数,只有函数体:

对比

  1. 与我们已知的其他方法相比,这种方法最大的不同在于,它实际上是通过运行时通过参数传递过来的字符串创建的
  2. 以前的所有声明方法都需要我们 —— 程序员,在脚本中编写函数的代码
  3. 但是 new Function 允许我们将任意字符串变为函数
    1. 例如,我们可以从服务器接收一个新的函数并执行它:

闭包

  1. 通常,闭包是指使用一个特殊的属性 [[Environment]] 来记录函数自身的创建时的环境的函数
    1. 它具体指向了函数创建时的词法环境
  2. 但是如果我们使用 new Function 创建一个函数,那么该函数的 [[Environment]] 并不指向当前的词法环境,而是指向全局环境
    1. 因此,此类函数无法访问外部(outer)变量,只能访问全局变量

  1. 如果这个函数能够访问外部(outer)变量会怎么样?
    1. 问题在于,在将 JavaScript 发布到生产环境之前,需要使用 压缩程序(minifier) 对其进行压缩
      一个特殊的程序,通过删除多余的注释和空格等压缩代码 —— 更重要的是,将局部变量命名为较短的变量
    2. 例如,如果一个函数有 let userName,压缩程序会把它替换为 let a(如果 a 已被占用了,那就使用其他字符),剩余的局部变量也会被进行类似的替换
    3. 一般来说这样的替换是安全的,毕竟这些变量是函数内的局部变量,函数外的任何东西都无法访问它
    4. 在函数内部,压缩程序会替换所有使用了这些变量的代码
      压缩程序很聪明,它会分析代码的结构,因此它不会“破坏”你的程序
    5. 但是在这种情况下,如果使 new Function 可以访问自身函数以外的变量,它也很有可能无法找到重命名的 userName,这是因为新函数的创建发生在代码压缩以后,变量名已经被替换了
    6. 所以,即使我们可以在 new Function 中访问外部词法环境,我们也会受挫于压缩程序

setTimeoutsetInterval

概述

  1. 有时我们并不想立即执行一个函数,而是等待特定一段时间之后再执行
    1. 这就是所谓的“计划调用(scheduling a call)”
  2. 目前有两种方式可以实现:
    1. setTimeout 允许我们将函数推迟到一段时间间隔之后再执行
    2. setInterval 允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数
  3. 这两个方法并不在 JavaScript 的规范中
    1. 但是大多数运行环境都有内建的调度程序,并且提供了这些方法
    2. 目前来讲,所有浏览器以及 Node.js 都支持这两个方法

setTimeout

  1. 语法
    1. func|code
      想要执行的函数或代码字符串
      一般传入的都是函数。由于某些历史原因,支持传入代码字符串,但是不建议这样做
    2. delay
      执行前的延时,以毫秒为单位(1000 毫秒 = 1 秒),默认值是 0
    3. arg1arg2…
      要传入被执行函数(或代码字符串)的参数列表(IE9 以下不支持,见2

  1. 如果第一个参数位传入的是字符串,JavaScript 会自动为其创建一个函数
    1. 但是,不建议使用字符串,我们可以使用箭头函数代替它们,如下所示:

注意1

  1. 新手开发者有时候会误将一对括号 () 加在函数后面:
    1. 这样不行,因为 setTimeout 期望得到一个对函数的引用
    2. 而这里的 sayHi() 很明显是在执行函数,所以实际上传入 setTimeout 的是 函数的执行结果
    3. 在这个例子中,sayHi() 的执行结果是 undefined(也就是说函数没有返回任何结果),所以实际上什么也没有调度

clearTimeout取消调度

  1. setTimeout 在调用时会返回一个“定时器标识符(timer identifier)”
    1. 在我们的例子中是 timerId,我们可以使用它来取消执行

  1. 下面的代码中,我们对一个函数进行了调度,紧接着取消了这次调度(中途反悔了)

  1. 关于定时器标识符
    1. alert 的输出来看,在浏览器中,定时器标识符是一个数字
    2. 在其他环境中,可能是其他的东西。例如 Node.js 返回的是一个定时器对象,这个对象包含一系列方法
    3. 重申一遍,这些方法没有统一的规范定义,所以这没什么问题

setInterval

  1. setInterval 方法和 setTimeout 的语法相同:
    1. 所有参数的意义也是相同的
    2. 不过与 setTimeout 只执行一次不同,setInterval 是每间隔给定的时间周期性执行

  1. 想要阻止后续调用,我们需要调用 clearInterval(timerId)
  2. 示例:将每间隔 2 秒就会输出一条消息。5 秒之后,输出停止:

注意2

  1. alert 弹窗显示的时候计时器依然在进行计时
    1. 在大多数浏览器中,包括 ChromeFirefox,在显示 alert/confirm/prompt 弹窗时,内部的定时器仍旧会继续“嘀嗒”
    2. 所以,在运行上面的代码时,如果在一定时间内没有关掉 alert 弹窗,那么在你关闭弹窗后,下一个 alert 会立即显示。两次 alert 之间的时间间隔将小于 2

嵌套的 setTimeout

  1. 周期性调度有两种方式
    1. 一种是使用 setInterval
    2. 另外一种就是嵌套的 setTimeout
  2. 示例
    1. 这个 setTimeout 在当前这一次函数执行完时 (*) 立即调度下一次调用

  1. 嵌套的 setTimeout 要比 setInterval 灵活得多,采用这种方式可以根据当前执行结果来调度下一次调用,因此下一次调用可以与当前这一次不同
    1. 例如,我们要实现一个服务(server),每间隔 5 秒向服务器发送一个数据请求,但如果服务器过载了,那么就要降低请求频率,比如将间隔增加到 102040 秒等
    2. 并且,如果我们调度的函数占用大量的 CPU,那么我们可以测量执行所需要花费的时间,并安排下次调用是应该提前还是推迟

  1. 嵌套的 setTimeout 相较于 setInterval 能够更精确地设置两次执行之间的延时
    1. 使用 setInterval 时,func 函数的实际调用间隔要比代码中设定的时间间隔要短!
    2. 这也是正常的,因为 func 的执行所花费的时间“消耗”了一部分间隔时间
    3. 也可能出现这种情况,就是 func 的执行所花费的时间比我们预期的时间更长,并且超出了 100 毫秒
    4. 在这种情况下,JavaScript 引擎会等待 func 执行完成,然后检查调度程序,如果时间到了,则 立即 再次执行它
      极端情况下,如果函数每次执行时间都超过 delay 设置的时间,那么每次调用之间将完全没有停顿
    5. 而嵌套的 setTimeout 就能确保延时的固定(这里是 100 毫秒)

注意3

  1. 垃圾回收和 setInterval/setTimeout 回调(callback
    1. 当一个函数传入 setInterval/setTimeout 时,将为其创建一个内部引用,并保存在调度程序中
    2. 这样,即使这个函数没有其他引用,也能防止垃圾回收器(GC)将其回收
    3. 对于 setInterval,传入的函数也是一直存在于内存中,直到 clearInterval 被调用

零延时的 setTimeout

  1. 有一种特殊的用法:setTimeout(func, 0),或者仅仅是 setTimeout(func)
    1. 这样调度可以让 func 尽快执行
  2. 但是只有在当前正在执行的脚本执行完成后,调度程序才会调用它
    1. 也就是说,该函数被调度在当前脚本执行完成“之后”立即执行
    2. 第一行代码“将调用安排到日程(calendar0 毫秒处”
    3. 但是调度程序只有在当前脚本执行完毕时才会去“检查日程”,所以先输出 "Hello",然后才输出 "World"

注意4

  1. 零延时实际上不为零(在浏览器中)
    1. 在浏览器环境下,嵌套定时器的运行频率是受限制的
    2. 根据 HTML5 标准 所讲:“经过 5 重嵌套定时器之后,时间间隔被强制设定为至少 4 毫秒”
  2. 示例
    1. 第一次,定时器是立即执行的(正如规范里所描述的那样)
    2. timer 数组里存放的是每次定时器运行的时刻与 start 的差值,所以数字只会越来越大,实际上前后调用的延时是数组值的差值(示例中前几次都是 1,所以延时为 0
    3. 如果我们使用 setInterval 而不是 setTimeout,也会发生类似的情况:setInterval(f) 会以零延时运行几次 f,然后以 4 毫秒以上的强制延时运行
    4. 这个限制来自“远古时代”,并且许多脚本都依赖于此,所以这个机制也就存在至今

  1. 对于服务端的 JavaScript,就没有这个限制,并且还有其他调度即时异步任务的方式
    1. 例如 Node.jssetImmediate
    2. 因此,这个提醒只是针对浏览器环境的

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享