二刷红宝书 -- 易错语法集锦(持续更新)

369 阅读5分钟

经典之var let const的区别

  • 作用域不同
    • var是函数作用域 存在声明提升
    • let const 是块级作用域
      • let存在暂时性死区(在let const声明变量之前使用变量都会抛出ReferenceError)
      • const在初始化时必须赋值,不能修改const声明的变量
  • 全局声明
    • var在全局声明时会成为window对象的属性,let const不会
    • let const声明仍然是在全局作用域发生的,变量会在页面的生命周期内延续

实例 : for循环中的var,let声明

for(var i = 0; i < 5; ++i){
    console.log('inter')
  setTimeout(()=>{
    console.log(i)
  }, 0)
}
console.log(`--outer ${i} outer--`)

输出 => --outer 5 -- 5 5 5 5 5

  • var是函数作用域,在for循环中声明的i会污染全局变量i
  • 在退出for循环时,迭代变量还是保存的导致循环退出的值即5 所以输出5个5
for(let i = 0; i < 5; ++i){
  setTimeout(()=>{
    console.log(i)
  }, 0)
}
console.log(i)

输出 => ReferenceError: i is not defined 0 ,1,2,3,4

  • let是块级作用域,只作用于for循环内部 所以报错
  • let会循环声明新的迭代变量 每个setTimeout都是引用不同的变量实例

最佳实践

  • 开发中尽量不适用var 一定能用let 或const取代
  • const优先 let次之

经典之 0.1 + 0.2 == 0.3

显然是return false 这是因为二进制模拟十进制进行计算时的精度问题,JavaScript是使用 IEEE 754 浮点数标准来表示数字,这种二进制浮点表示法有时无法精确地表示十进制小数

可以用一个可允许误差Number.EPSILON来判断

const equal = (num1, num2, num3) => (num1 + num2) - num3 < Number.EPSILON ? true : false 

一元加和减操作符

如果将一元加减应用到非数值,则会执行与使用 Number()转型函数一样的类型转换:

  • 布尔值 false 和 true 转换为 0 和 1
  • 字符串 如果是有效的数值形式,变量类型从字符串变成数值,否则将变量的值设置为 NaN
  • 对象会调用它们的 valueOf()方法以得到可以转换的值,如果是 NaN,则调用 toString()并再次应用其他规则
let s1 = "2";
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = {
    valueOf() {
    return -1;
} };
s1 = +s1; // 值变成数值1
s2 = -s2; // 值变成数值-1.1
s3 = -s3;  // 值变成NaN
b = -b;// 值变成数值 0 
f = -f;  // 变成-1.1
o = +o; // 值变成数值 -1

加法操作符转换规则

  • 如果两个操作数都是数值,加法操作符执行加法运算
  • 如果有任一操作数是 NaN,则返回 NaN; 不过,如果有一个操作数是字符串,则要应用如下规则:
  • 如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面;
  • 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起。 如果有任一操作数是对象、数值或布尔值,则调用它们的 toString()方法以获取字符串,然后再应用前面的关于字符串的规则。对于 undefined 和 null,则调用 String-函数,分别获取 "undefined"和"null"。

例如

console.log(5 + 5) // 10
console.log(5 + "5") // '55'
let num1 = 5;
let num2 = 10;
console.log( "The sum of 5 and 10 is " + num1 + num2);  // "The sum of 5 and 10 is 510"  如果(num1 + num2)就正常了

减法操作符转换规则

  • 如果两个操作数都是数值,则执行数学减法运算并返回结果
  • 如果有任一操作数是 NaN,则返回 NaN。 如果有任一操作数是字符串、布尔值、null 或 undefined,则先在后台使用 Number()将其转换为数值,然后再根据前面的规则执行数学运算。如果转换结果是 NaN,则减法计算的结果是NaN
  • 如果有任一操作数是对象,则调用其 valueOf()方法取得表示它的数值。如果该值是 NaN,则减法计算的结果是 NaN。如果对象没有 valueOf()方法,则调用其 toString()方法,然后再将得到的字符串转换为数值。
let result1 = 5 - true; // true被转换为1,所以结果是4 
let result2 = NaN - 1; // NaN
let result3=5-3;  //2
let result4 = 5 - "";  // ""被转换为0,所以结果是5 
let result5 = 5 - "2"; // "2"被转换为2,所以结果是3 
let result6 = 5 - null; // null被转换为0,所以结果是5

== 和 ===之区别

== 是等于 ===是全等

== 等于操作符转换规则

ECMAScript中的等于操作符用两个等于号(==)表示,如果操作数相等,则会返回true。那么操作数是怎么来的呢,因为等于操作符都会进行类型转换(通常为强制类型转换) 转换规则如下:

  • 如果任一操作数是布尔值,则将其转换为数值再比较是否相等。false 转换为 0,true 转换为1
  • 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等
  • 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法取得其原始值,再根据前面的规则进行比较
  • 如果有任一操作数是NaN,则相等操作符返回false,不相等操作符返回true。因为按照规则,NaN 不等于 NaN
  • 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true 否则false

一些特殊情况及比较结果 Img

全等操作符

全等操作符在比较相等时不转换操作数,只有两个操作数在不转换的前提下相等才返回true

1et result1 = ("55" == 55);//true,转换后相等 
let result2 = ("55" === 55);//fa1se,不相等,因为数据类型不同
let result3 = (undefined == null) // true
let result4 = (undefined === null) // false

最佳实践

由于相等和不相等操作符存在类型转换问题,因此推荐使用全等和不全等操作符, 这样有助于在代码中保持数据类型的完整性

with语句

with语句的用途是代码作用于设置为特定的对象,其语法是:
with(expression) statement; 使用with语句的主要场景是针对一个对象反复操作,这时候将代码作用域设置为该对象能提供便利,如下面的例子所示:

let qs = location.search.substring(1)
let hostName = location.hostName
let url = location.href

<!-- ==============> -->
with(location){    
    let qs = search.substring(1)
    let hostName = hostName
    let url = href
}

这里with语句用于链接location对象,这意味这这个语句内部,每个变量首先会被认为是一个局部变量,如果没有找到该局部变量则会搜索location对象,是否有一个同名的属性,如果有,则改变量会被求值为with语句,否则会抛出错误

最佳实践

虽然with有一定的方便之处,但其更影响性能且难于调试其中的代码,通常不推荐使用

原始值与引用值

在把一个值赋给变量时,JavaScript 引擎必须确定这个值是原始值还是引用

  • 七种原始值是Undefined、Null、Boolean、Number、String、BigInt 和 Symbol。保存原始值的变量是按值(by value)访问的,因为我们操作的就是存储在变量中的实际值。
  • 引用值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接访问内存位置,因此也就 不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用(reference)而非 实际的对象本身。为此,保存引用值的变量是按引用(by reference)访问的。

动态属性

引用值可以随时添加、修改和删除其属性 和方法,而原始值不能有属性

举个例子 我们分别用String的构造函数和字面量形式给string变量赋值会得到不同结果

const string = new String("hello world")
string.name = 'Bob'
console.log(string.name) //Bob
const string = "hello world"
string.name = 'Bob'
console.log(string.name) //undefined

如果使用的是 new 关键字则 JavaScript 会创建一个 Object类型的实例,但其行为类似原始值。下面来看看这两种初始化方式的差异:

let name1 = "Bob";
let name2 = new String("Herry"); 
name1.age = 27;
name2.age = 26;
console.log(name1.age);    // undefined
console.log(name2.age);    // 26
console.log(typeof name1); // string
console.log(typeof name2); // object

复制值

在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置,而引用值其实是复制的是指针,指向存储在堆内存中的对象

举个例子 这两个变量可以独立使用,互不干扰

let num1 = 5;
let num2 = num1;

Img

把引用值从一个变量赋给另一个变量时,两个变量实际上指向同一个对象

let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); // "Nicholas"

Img

传参

函数的参数都是按值传递的,这意味着函数外的值会被复制到函数内部的参数 中,就像从一个变量复制到另一个变量一样。如果是原始值,就类比上面赋值值<原始值>,如果是引用值,就类比赋值<引用值>

也还是举个简单例子

// 原始值
const addOne = (num) => ++num
let account = 10
console.log(addTen(10)) //11
console.log(account) // 10 没有改变

// 引用值
const changeName = (person) => person.name = 'herry'
const person = new Object()
changeName(person)
console.log(person.name) // herry 改变值咯

垃圾回收

JavaScript是自动内存管理实现内存分配和闲置资源回收,基本实现思路是:确定哪个变量不会再使用,然后释放它占用的内存 难点在于如何标记未使用的变量,主要策略是标记清理和引用计数

标记清理(mark-and-sweep)

JavaScript 最常用的垃圾回收策略是标记清理。当变量进入上下文,比如在函数内部声明一个变量时这个变量会被加上存在于上下文中的标记。而在上下文中的变量不会释放它们的内存,因为只要上下文中的代码在运行就有可能用到它们。当变量离开上下文时也会被加上离开上下文的标记。标记过程的实现并不重要,关键是策略

引用计数(reference counting)

引用计数相对没有那么常用。其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。当一 个值的引用数为0时就说明没办法再访问到这个值了,因此可以安全地收回其内存了

参考

《JavaScript 高级程序设计 第四版》