JavaScript-全局对象
全局对象
全局对象提供可在任何地方使用的变量和函数。默认情况下,这些全局变量内建于语言或环境中。
在浏览器中,它的名字是 “window”,对 Node.js 而言,它的名字是 “global”,其它环境可能用的是别的名字。
最近,globalThis
被作为全局对象的标准名称加入到了 JavaScript 中,所有环境都应该支持该名称。所有主流浏览器都支持它。
假设我们的环境是浏览器,我们将在这儿使用 “window”。如果你的脚本可能会用来在其他环境中运行,则最好使用 globalThis
。
全局对象的所有属性都可以被直接访问:
alert("Hello");
// 等同于
window.alert("Hello");
在浏览器中,使用 var
(而不是 let/const
!)声明的全局函数和变量会成为全局对象的属性。
var gVar = 5;
alert(window.gVar); // 5(成为了全局对象的属性)
具有与函数声明相同的效果(在主代码流中具有 function
关键字的语句,而不是函数表达式)。
请不要依赖它!这种行为是出于兼容性而存在的。现代脚本使用 JavaScript modules 所以不会发生这种事情。
如果我们使用 let
,就不会发生这种情况:
let gLet = 5;
alert(window.gLet); // undefined(不会成为全局对象的属性)
如果一个值非常重要,以至于你想使它在全局范围内可用,那么可以直接将其作为属性写入:
// 将当前用户信息全局化,以允许所有脚本访问它
window.currentUser = {
name: "John"
};
// 代码中的另一个位置
alert(currentUser.name); // John
// 或者,如果我们有一个名为 "currentUser" 的局部变量
// 从 window 显式地获取它(这是安全的!)
alert(window.currentUser.name); // John
也就是说,一般不建议使用全局变量。全局变量应尽可能的少。与使用外部变量或全局变量相比,函数获取“输入”变量并产生特定“输出”的代码设计更加清晰,不易出错且更易于测试。
使用 polyfills
我们使用全局对象来测试对现代语言功能的支持。
例如,测试是否存在内建的 Promise
对象(在版本特别旧的浏览器中不存在):
if (!window.Promise) {
alert("Your browser is really old!");
}
如果没有(例如,我们使用的是旧版浏览器),那么我们可以创建 “polyfills”:添加环境不支持但在现代标准中存在的功能。
if (!window.Promise) {
window.Promise = ... // 定制实现现代语言功能
}
总结
-
全局对象包含应该在任何位置都可见的变量。
其中包括 JavaScript 的内建方法,例如 “Array” 和环境特定(environment-specific)的值,例如
window.innerHeight
— 浏览器中的窗口高度。 -
全局对象有一个通用名称
globalThis
。……但是更常见的是使用“老式”的环境特定(environment-specific)的名字,例如
window
(浏览器)和global
(Node.js)。 -
仅当值对于我们的项目而言确实是全局的时候,才应将其存储在全局对象中。并保持其数量最少。
-
在浏览器中,除非我们使用 【modules】,否则使用
var
声明的全局函数和变量会成为全局对象的属性。 -
为了使我们的代码面向未来并更易于理解,我们应该使用直接的方式访问全局对象的属性,如
window.x
。
函数对象,NFE
我们已经知道,在 JavaScript 中,函数就是值。
JavaScript 中的每个值都有一种类型,那么函数是什么类型呢?
在 JavaScript 中,函数就是对象。
一个容易理解的方式是把函数想象成可被调用的“行为对象(action object)”。我们不仅可以调用它们,还能把它们当作对象来处理:增/删属性,按引用传递等。
属性 “name”
函数对象包含一些便于使用的属性。
比如,一个函数的名字可以通过属性 “name” 来访问:
function sayHi() {
alert("Hi");
}
alert(sayHi.name); // sayHi
更有趣的是,名称赋值的逻辑很智能。即使函数被创建时没有名字,名称赋值的逻辑也能给它赋予一个正确的名字,然后进行赋值:
let sayHi = function() {
alert("Hi");
};
alert(sayHi.name); // sayHi(有名字!)
当以默认值的方式完成了赋值时,它也有效:
function f(sayHi = function() {}) {
alert(sayHi.name); // sayHi(生效了!)
}
f();
规范中把这种特性叫做「上下文命名」。如果函数自己没有提供,那么在赋值中,会根据上下文来推测一个。
对象方法也有名字:
let user = {
sayHi() {
// ...
},
sayBye: function() {
// ...
}
}
alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye
这没有什么神奇的。有时会出现无法推测名字的情况。此时,属性 name
会是空,像这样:
// 函数是在数组中创建的
let arr = [function() {}];
alert( arr[0].name ); // <空字符串>
// 引擎无法设置正确的名字,所以没有值
而实际上,大多数函数都是有名字的。
属性 “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 参数不参与计数。
属性 length
有时在操作其它函数的函数中用于做 内省/运行时检查(introspection)。
比如,下面的代码中函数 ask
接受一个询问答案的参数 question
和可能包含任意数量 handler
的参数 ...handlers
。
当用户提供了自己的答案后,函数会调用那些 handlers
。我们可以传入两种 handlers
:
- 一种是无参函数,它仅在用户回答给出积极的答案时被调用。
- 一种是有参函数,它在两种情况都会被调用,并且返回一个答案。
为了正确地调用 handler
,我们需要检查 handler.length
属性。
我们的想法是,我们用一个简单的无参数的 handler
语法来处理积极的回答(最常见的变体),但也要能够提供通用的 handler:
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));
这种特别的情况就是所谓的 [多态性](多态性_百度百科 (baidu.com))) —— 根据参数的类型,或者根据在我们的具体情景下的 length
来做不同的处理。这种思想在 JavaScript 的库里有应用。
自定义属性
我们也可以添加我们自己的属性。
这里我们添加了 counter
属性,用来跟踪总的调用次数:
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
是毫不相关的两个东西。
我们可以把函数当作对象,在它里面存储属性,但是这对它的执行没有任何影响。变量不是函数属性,反之亦然。它们之间是平行的。
函数属性有时会用来替代闭包。例如,我们可以使用函数属性将 【变量作用域,闭包】章节中 counter 函数的例子进行重写:
function makeCounter() {
// 不需要这个了
// let count = 0
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
现在 count
被直接存储在函数里,而不是它外部的词法环境。
那么它和闭包谁好谁赖?
两者最大的不同就是如果 count
的值位于外层(函数)变量中,那么外部的代码无法访问到它,只有嵌套的函数可以修改它。而如果它是绑定到函数的,那么就很容易:
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),指带有名字的函数表达式的术语。
例如,让我们写一个普通的函数表达式:
let sayHi = function(who) {
alert(`Hello, ${who}`);
};
然后给它加一个名字:
let sayHi = function func(who) {
alert(`Hello, ${who}`);
};
我们这里得到了什么吗?为它添加一个 "func"
名字的目的是什么?
首先请注意,它仍然是一个函数表达式。在 function
后面加一个名字 "func"
没有使它成为一个函数声明,因为它仍然是作为赋值表达式中的一部分被创建的。
添加这个名字当然也没有打破任何东西。
函数依然可以通过 sayHi()
来调用:
let sayHi = function func(who) {
alert(`Hello, ${who}`);
};
sayHi("John"); // Hello, John
关于名字 func
有两个特殊的地方,这就是添加它的原因:
- 它允许函数在内部引用自己。
- 它在函数外是不可见的。
例如,下面的函数 sayHi
会在没有入参 who
时,以 "Guest"
为入参调用自己:
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
进行嵌套调用?
当然,在大多数情况下我们可以这样做:
let sayHi = function(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
sayHi("Guest");
}
};
上面这段代码的问题在于 sayHi
的值可能会被函数外部的代码改变。如果该函数被赋值给另外一个变量(译注:也就是原变量被修改),那么函数就会开始报错:
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
。
我们给函数表达式添加的可选的名字,正是用来解决这类问题的。
让我们使用它来修复我们的代码:
let sayHi = function func(who) {
if (who) {
alert(`Hello, ${who}`);
} else {
func("Guest"); // 现在一切正常
}
};
let welcome = sayHi;
sayHi = null;
welcome(); // Hello, Guest(嵌套调用有效)
现在它可以正常运行了,因为名字 func
是函数局部域的。它不是从外部获取的(而且它对外部也是不可见的)。规范确保它只会引用当前函数。
外部代码仍然有该函数的 sayHi
或 welcome
变量。而且 func
是一个“内部函数名”,可用于函数在自身内部进行自调用。
ℹ️函数声明没有这个东西
这里所讲的“内部名”特性只针对函数表达式,而不是函数声明。对于函数声明,没有用来添加“内部”名的语法。
有时,当我们需要一个可靠的内部名时,这就成为了你把函数声明重写成函数表达式的理由了。
总结
函数就是对象。
我们介绍了它们的一些属性:
name
—— 函数的名字。通常取自函数定义,但如果函数定义时没设定函数名,JavaScript 会尝试通过函数的上下文猜一个函数名(例如把赋值的变量名取为函数名)。length
—— 函数定义时的入参的个数。Rest 参数不参与计数。
如果函数是通过函数表达式的形式被声明的(不是在主代码流里),并且附带了名字,那么它被称为命名函数表达式(Named Function Expression)。这个名字可以用于在该函数内部进行自调用,例如递归调用等。
此外,函数可以带有额外的属性。很多知名的 JavaScript 库都充分利用了这个功能。
它们创建一个“主”函数,然后给它附加很多其它“辅助”函数。例如,jQuery 库创建了一个名为 $
的函数。lodash 库创建一个 _
函数,然后为其添加了 _.add
、_.keyBy
以及其它属性(想要了解更多内容,参查阅 docs)。实际上,它们这么做是为了减少对全局空间的污染,这样一个库就只会有一个全局变量。这样就降低了命名冲突的可能性。
所以,一个函数本身可以完成一项有用的工作,还可以在自身的属性中附带许多其他功能。
✅任务
为 counter 添加 set 和 decrease 方法
重要程度:five:
修改 makeCounter()
代码,使得 counter 可以进行减一和设置值的操作:
counter()
应该返回下一个数字(与之前的逻辑相同)。counter.set(value)
应该将count
设置为value
。counter.decrease()
应该把count
减 1。
查看沙箱中的代码获取完整使用示例。
P.S. 你可以使用闭包或者函数属性来保持当前的计数,或者两种都写。
解决方案
该解决方案在局部变量中使用
count
,而进行加法操作的方法是直接写在counter
中的。它们共享同一个外部词法环境,并且可以访问当前的count
。function makeCounter() { let count = 0; function counter() { return count++; } counter.set = value => count = value; counter.decrease = () => count--; return counter; }
任意数量的括号求和
重要程度:two:
写一个函数 sum
,它有这样的功能:
sum(1)(2) == 3; // 1 + 2
sum(1)(2)(3) == 6; // 1 + 2 + 3
sum(5)(-1)(2) == 6
sum(6)(-1)(-2)(-3) == 0
sum(0)(1)(2)(3)(4)(5) == 15
P.S. 提示:你可能需要创建自定义对象来为你的函数提供基本类型转换。
解决方案
- 为了使整个程序无论如何都能正常工作,
sum
的结果必须是函数。- 这个函数必须将两次调用之间的当前值保存在内存中。
- 根据这个题目,当函数被用于
==
比较时必须转换成数字。函数是对象,所以转换规则会按照 对象 — 原始值转换 章节所讲的进行,我们可以提供自己的方法来返回数字。代码如下:
function sum(a) { let currentSum = a; function f(b) { currentSum += b; return f; } f.toString = function() { return currentSum; }; return f; } alert( sum(1)(2) ); // 3 alert( sum(5)(-1)(2) ); // 6 alert( sum(6)(-1)(-2)(-3) ); // 0 alert( sum(0)(1)(2)(3)(4)(5) ); // 15
请注意
sum
函数只工作一次,它返回了函数f
。然后,接下来的每一次子调用,
f
都会把自己的参数加到求和currentSum
上,然后f
自身。在
f
的最后一行没有递归。递归是这样子的:
function f(b) { currentSum += b; return f(); // <-- 递归调用 }
在我们的例子中,只是返回了函数,并没有调用它:
function f(b) { currentSum += b; return f; // <-- 没有调用自己,只是返回了自己 }
这个
f
会被用于下一次调用,然后再次返回自己,按照需要重复。然后,当它被用做数字或字符串时 ——toString
返回currentSum
。我们也可以使用Symbol.toPrimitive
或者valueOf
来实现转换。
- 点赞
- 收藏
- 关注作者
评论(0)