JavaScript 随记(1)

43 阅读7分钟

类型、值和变量

1、直接调用函数和 new 一个函数的区别

直接调用:

  • 函数中的 this 关键字指向 Windows(在浏览器中);

  • 函数的返回值取决于函数本身的实现,若无显式地 return,则会返回 undefined。

使用 new 关键字:

  • 将创建一个新的空对象;

  • 函数中的 this 关键字指向这个新创建的空对象;

  • 函数的返回值为这个新对象的引用,除非显式地返回另一个对象;

  • 此时的函数被称为构造函数(constructor)。

2、算数运算

  • 溢出(overflow):大于 Number.MAX_VALUE,或小于 -Number.MAX_VALUE 时返回 ±Infinity

  • 下溢(underflow):小于 Number.MIN_VALUE,或小于 -Number.MIN_VALUE 时返回 ±0

  • 0 除任何数结果为 0;正数除 0 结果为 Infinity;负数除 0 结果为 -Infinity;0 除 0 没有意义,结果为 NaN(Not a Number)

  • NaN 与任何值都不相等,包括它自身,因此使用 x != x 来判断一个变量是否为 NaN;也可以直接用函数 isNaN()

  • ±0严格相等的,这意味着它们除了在运算时改变 ± 号外,几乎一模一样。

  • JavaScript 中的所有数都是用浮点数值表示的(IEEE-754 浮点数表示法),使用实数时往往只是一个真实值的近似表示,这就导致高精度的计算出现误差(例如 .3 - .2 != .2 - .1)。

3、字符串

  • 虽然字符串可以像数组一样按索引访问,但 JavaScript 中的字符串是不允许修改的。replace()toUpperCase() 之类的方法都是返回新的字符串,并不会修改原有字符串。

4、null 与 undefined

  • typeof(null) === "object", typeof(undefined) === "undefined"

  • null == undefined, null !== undefined

5、全局对象

  • JavaScript 解释器(浏览器或 Node.js 中的 V8 引擎之类的)在运行 JavaScript 时,会创建一个全局对象,并赋值一组定义的初始属性。

全局属性举例.png

6、包装对象

JavaScript 只有引用类型的变量上存在属性和方法,那么为什么像 number, string, boolean 这样的原始类型也有属性和方法可以调用呢?

  • string 为例,调用一个字符串变量上的属性和方法时,JavaScript 会通过 new String(s) 的方式将其转换为一个临时对象属性和方法其实是在 String 类实例化出来的这个临时对象上的

  • 当属性或方法的引用结束后,这个临时对象就会被回收;

  • 包装对象上的属性都是只读的,并且不能定义新的属性

  • 原始类型的变量与其对应的包装对象,相等,但不全等

7、类型转换

  • 虽然 typeof(null) === "object",但将 null 转换为 Object抛出异常

JavaScript 类型转换.png

  • 注意:上述的转换并不意味着两个值相等! 例如 if 语句会将 undefined 转换为 false,但在使用 == 运算符作比较时并不会将 undefined 转换为 false== 运算符从不试图将其操作数转换为布尔值!

8、对象到原始值的转换

  • 转换为布尔值:所有的对象(包括数组和函数)都将转换为 true;同样包括了包装对象,例如 new Boolean(false) 转换为布尔值也是 true

注意:接下来的结论仅针对于本地对象,不包括宿主对象(JavaScript 运行环境中提供的对象)。

  • 转换为字符串:所有的对象都继承了一个 toString() 方法,它的作用是返回一个反应这个对象的字符串。很多类定义了不同版本的 toString() 方法,如数组、函数、正则、日期类......
类型规则例子
数组类将每一个数组元素转换为字符串,并在元素之间添加逗号后合并成结果字符串。[1, 2, 3] => "1,2,3"
函数类通常是将函数转换为源代码字符串。function () { return 0; } => "function () { return 0; }"
正则类直接转换为字符串。/\d+/g => "/\d+/g"
日期类经过 JavaScript 解析,返回一个可读的日期和时间字符串。new Date(2024, 5, 7) => 'Fri Jun 07 2024 00:00:00 GMT+0800 (中国标准时间)'
  • 转换为数:所有的对象都继承了一个 valueOf() 方法,但大多数对象都无法真正地转换为一个原始值,所以它的作用只是简单地返回对象本身。其中,日期类valueOf() 方法会返回对象的一个内部表示:new Date(2024, 5, 7).valueOf() => 1717689600000

  • 对象在转换成字符串或数的底层逻辑:优先调用 toString() / valueOf(),若对象上不存在该方法或没有返回一个原始值,则再调用 valueOf() / toString() 方法,若仍失败,则抛出一个类型错误异常。举个例子:数组转换成数时,优先调用 valueOf() 方法,返回数组本身,不满足要求,因此调用 toString() 方法,再将字符串结果转换为数。

9、声明提前

使用 var 声明的变量在整个函数体中都是存在声明的(仅针对声明,不包括赋值)。

var scope = "global";
function f () {
	console.log(scope);		// 1
	var scope = "local";
	console.log(scope);		// 2
}

上述代码中,1 将输出 undefined,2 将输出 "local",因为声明提前了,但赋值并不会提前

上述代码的效果等价于:

var scope = "global";
function f () {
	var scope;
	console.log(scope);
	scope = "local";
	console.log(scope);
}

10、作用域、作用域链、词法环境、执行上下文、调用栈

  • JavaScript 是基于词法作用域(也叫 静态作用域)的语言,变量的作用域在代码编写时已经确定了,在代码执行时不会再发生变化。

  • 作用域在代码块中定义变量的区域,决定了变量的可访问性

var a = 1;
function fnc () {
	var b = 2;
	for (let i = 0; i < b; i += a) {
		console.log(" i ", i);
	}
}

fnc();

作用域是对于变量而言的。在上述代码中,变量 a 的作用域为全局作用域,在所有地方都可以访问;变量 b 的作用域为函数作用域,仅在函数 fnc 中可以访问;变量 i 的作用域为块级作用域,仅在 for 循环代码块中可以访问。

  • 词法环境:词法环境是对于 JavaScript 代码(全局代码或函数)而言的,在 JavaScript 代码执行之前创建。词法环境的结构分为两部分:1、存储代码内部的函数、变量的声明以及对应的标识符;2、指向外部其它词法环境的引用

  • 作用域链:多个词法环境组成的链式结构便称为作用域链。作用域链是对执行上下文而言的。

仍然以上述代码为例,在 for 循环代码块中引用的变量 a, b,是根据作用域链依次寻找的。首先找到函数 fnc 的词法环境,这里存储了变量 b, i 的声明;然后根据词法环境的引用继续向外部词法环境寻找,找到全局的词法环境,这里存储了变量 a 和函数 fnc 的声明。

  • 执行上下文:执行上下文也是对于 JavaScript 代码(全局代码或函数)而言的,在 JavaScript 的执行阶段创建。其中包含了关于代码执行的一些重要信息:当前的词法环境,变量的值,作用域链,this 指向等等。

  • 调用栈:调用栈是一个用来管理执行上下文的栈结构,遵循后进先出(LIFO)规则。

仍然以上述代码为例,首先是最外层的全局执行上下文进栈,接下来是函数 fnc 的执行上下文进栈,函数执行完毕后出栈并销毁,最后是全局执行上下文出栈并销毁。

总结:

  • 作用域、词法环境是静态的;执行上下文是动态的。
  • 作用域链由词法环境构成;执行上下文中包含词法环境,也就包含作用域链。