【中卷】你不知道的JavaScript(一)

104 阅读6分钟

1、typeof 安全机制

  • undefinedundeclared是两码事,但是typeofundefinedundeclared 变量返回都是 undefined
  • undeclared变量在进行使用的时候,浏览器会报错(如下所示)
if(DEBUG){
  consoel.log("Debugging is starting");
}

VM635:1 Uncaught ReferenceError: DEBUG is not defined
    at <anonymous>:1:13

判断变量是否存在,typeof自带的安全机制,可避免报错

if(typeof DEBUG !== 'undefined'){
  consoel.log("Debugging is starting");
}

或者通过全局对象进行判断,即使变量DEBUG不存在,会返回undefinedboolean类型为false

if(window.DEBUG){
  // pass
}
  • 为某个脚本缺失的功能写polyfill,内部不能通过关键字var(声明提升)进行声明,防止与浏览器中一些特殊的内建全局变量冲突,重复声明会报错,去掉var可以防止声明被提升
if(typeof atob === 'undefined'){
  atob = function(){
    // pass
  }
}
  • let声明前引用会进入暂时性死区,typeof安全机制也不能避免
typeof a
let a  

2、数组

  • 数组使用delete运算符可以将单元从数组中删除,删除后数组长度不发生改变
  • 对数组未设置的单元,会显示通过undefined填充,但与直接设置a[1] =undefined 不同
let a = [];
a[0] = 0;
a[2] = 2;
console.log(a)                                // [0, empty, 2]
console.log([...a])                           // [0, undefined, 2]
console.log(JSON.stringify(a))                // [0, null, 2] 
  • 数组通过数字进行索引,同时也是对象,可以包含键值对,但是不计算在长度(length)之内
  • 我们一般不在数组中保存属性,但是Array.prototype是这么使用的,是一个空数组,并且有许多属性对应各自的方法
let a=[]
a[0] = 0
a[1] = 1
a['a'] = 'a'
a['b'] = 'b'
console.log(a, a.length)                      // [0, 1, a:'a', b: 'b']

注意:如果键值中有可以被强制转换为十进制数字,就被当做数字索引来处理

a['12'] = 1
console.log(a.length)                         // 13

3、字符串

  • 字符串经常被当成字符数组,都是类数组,本质并不相同,
  • 字符串可以借助数组函数处理字符串
var a = "foo" ;
var b = ["f", "o", "o"] ;
var c = Array.prototype.join.call( a, "-")
var d = Array.prototype.map.call( a, function(v){
  return v.toUpperCase() + '.'
}).join("")
console.log(c, d)                            //  f-o-o    F.O.O.

但是无法通过借助数组reverse方法翻转字符串

console.log(Array.prototype.reverse.call(a)) 
// Cannot assign to read only property '0' of object '[object String]'

字符串只可以借助部分数组方法,为什么呢?

  1. 字符串本身是不可变的,变异函数是在变量本身进行修改
  2. 数组方法(非变异函数)可以通过改变this指向处理字符串

转变思路 将字符串转换为数组,逆序后再将数组转变为字符串

var a = "foo"
var c = a.split("").reverse().join("")
console.log(c)                // "oof"

4、数字

  • 数字可以使用一些省略写法
var a = 0.42      =>     var a = .42
var b = 42.0      =>     var b = 42.
  • 所以数字在使用一些方法的时候,有歧义的情况下,会优先识别为数字的一部分,导致一些语法错误
// 无效语法
42.toFixed( 3 )               // SyntaxError 
// 正确语法
0.42.toFixed(3)               // 0.420
(42).toFixed(3)               // 42.000
42..toFixed(3)                // 42.000
42 .toFixed(3)                // 42.000    注意有空格哦😯
  • 小数精度问题
0.1 + 0.2 === 0.3             // false

计算机在存储的时候通过二进制编码,整数部分通过除以2取余,小数部分通过乘以2取整,对于无线循环的部分会进行截取(2进制,类似于四舍五入)

  • 机器精度(ES6新增)
if(!Niumber.EPSILON){
  Number.EPSILON = Math.pow(2, -52)
}
function numbersCloseEnoughToEqual(n1, n2){
  return Math.abs( n1- n2 ) < Number.EPSILON
}
  • 整数安全范围(ES6)
Number.MAX_VALUE                        1.79E+308
Number.MAX_SAFE_INTEGER                 Math.pow(2, 53) - 1
Number.MIN_SAFE_INTEGER             - ( Math.pow(2, 53) - 1 )

if (num1 * num2 <= Number.MAX_VALUE) {
   func1();
} else {
   func2();
}
  • NaN 不是数字的数字,仍然是数字类型
var num = 2 / "foo";                                   // NaN
typeof num === "number";                               // true 
NaN !== NaN                                            // true
isNaN(num)

Js提供了isNaN方法用于类型判断

Number.isNaN( "foo" )                                  // true

// 满足 isNaN方法 且 typeof 返回 number 的情况
if(!Number.isNaN){
  Number.isNaN = function(n){
    return ( typeof n === "number" && window.isNaN(n)  )
  }
}

//  NaN是 `Js` 中唯一一个不等于自身的值
if(!Number.isNaN){
  Number.isNaN = function(n){
    return n !== n
  }
}

5、值和引用

  • JavaScript 中没有指针,引用的工作机制也不尽相同。在JavaScript中变量不可能成为指针指向另一个变量的引用。
  • JavaScript 引用指向的是值。如果一个值有10个引用,这些引用指向的都是同一个值,它们相互之间没有引用 / 指向关系
var a = 2;
var b = a;
b++;
a ;             // 2
b ;             // 3

var c = [1, 2, 3] ;
var d = c ;
d.push(4);
c ;             // [1, 2, 3, 4]
d ;             // [1, 2, 3, 4]
  • 由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向
var a = [1, 2, 3]
var b = a
a ;             // [1, 2, 3]
b ;             // [1, 2, 3]

b = [4, 5, 6]
a ;             // [1, 2, 3]
b ;             // [4, 5, 6] 
  • 如果想通过b来修改a的值,应该在相同指向的时候进行对值修改,而非重新修改引用
var a = [1, 2, 3]
var b = a
a ;             // [1, 2, 3]
b ;             // [1, 2, 3]

b.length = 0
b.push(4, 5, 6)
a ;             // [4, 5, 6]
b ;             // [4, 5, 6] 
  • 然而基础类型不可修改
function foo(x){
  x = x + 1
  console.log(x)//  3
}
var a = 2 
foo(a)
console.log(a)  //  2
  • 不通过传参数的方式,函数内部使用的外部变量,是可以修改的
function foo(){
   x = x + 1
  console.log(x)  //  3
}
var x = 2
foo()
x // 3

引用功能强大,但有时也会成为阻碍,赋值 / 参数传递是通过引用还是值复制完全由值的类型来决定,所以使用那种类型也间接决定了赋值 / 参数传递的方式。 ####6、原生函数(内建函数) 原生函数可以被当做构造函数来进行使用,但是构造出来的对象和直接赋值的对象有所不同 new String('abc') 创建的是字符串"abc"的封装对象,而非基本类型值"abc"

var a = new String( "abc" )
console.log(a)                                        // String {'abc'}
console.log(typeof a);                                // object
console.log(a instanceof String);                     // true
console.log(Object.prototype.toString.call( a ));     // "[object String]"
let b = 'b'
console.log(b)                                        // b
console.log(typeof b);                                // string
console.log(b instanceof String);                     // false
console.log(Object.prototype.toString.call( b ))      // "[object String]" 
  • 于是就出现了奇怪的现象, 这里new关键字创建的是false的封装对象,而不是基本类型false本身。对象本身是真值
var a = new Boolean( false )  
if(!a){
  console.log( "I'm here!" )    // 执行不到这里
}
  • 那么如果想要获得封装对象中的基本类型值呢?(valueOf方法)
var a = new String( "abc" )
var b = new Number( 42 )
var c = new Boolean( true )

a.valueOf() ;   //  "abc"
b.valueOf() ;   //  42
c.valueOf() ;   //  true

7、toString

对象如果有自己的toString()方法,字符串化的时候会自动调用该方法,并使用其返回值。 相信有一个面试题可能大家都遇到过,实现一个方法,可以满足: add(1)(2)(3)(4,5)(6)

function add(...args){
  let sum = args.reduce((prev, cur)=>{return prev + cur}, 0)
  function add2(...args2){
    return add(sum, ...args2)
  }
  add2.toString = function(){
    return sum
  }
  return add2
}
console.log(add(1,2,3)(4)(5,6))    // 21

这个函数是可以实现的,但是浏览器兼容问题,以现在(2021-10-28)为例,

  • Safari (14.0.3) 可以正常执行= 22597286-5c2ccb3e94abd1dc.webp
  • chrome (95.0.4638.54)不能执行 2.webp
  • 火狐(93.0)也是不能执行的 网上有很多实现的案例,但是在多次调用之后,就会出现问题,详见:zhuanlan.zhihu.com/p/296852112

8、JSON.stringify

  • JSON.stringify()在对象中遇到undefinedfunctionsymbol时会自动忽略,在数组中则会返回null,保证元素位置不发生改变
JSON.stringify( undefined );                          // undefined
JSON.stringify( function(){} );                       // undefined
JSON.stringify( [1, undefined, function(){}, 4] )     // [1, null, null, 4]
JSON.stringify( { a:2, b: function(){}  })            // '{"a":2}'
  • JSON.stringify() 方法对于循环引用的对象会出错
  • 但是可以在对象中定义toJson()方法,序列化的时候会首先调用该方法返回安全值,然后对安全值进行序列化
let o = {};
let a = {
  b: 42,
  c: o,
  d: function(){ }
}
o.e = a  // 循环引用
JSON.stringify( a )        //  TypeError: Converting circular structure to JSON
a.toJSON = function(){
  return {b: this.b}
}
JSON.stringify( a );       //  '{"b":42}'  
  • JSON.stringify()可以通过可选参数,指定部分属性被序列化
let a = {
  b: 42, 
  c: "42",
  d: [1, 2, 3]
}
JSON.stringify( a, ["b", "c"])          //   {"b":42,"c":"42"}
JSON.stringify(a, function(k, v){
  if(k !== 'd'){
    return v
  }
})                                      //  {"b":42,"c":"42"}
  • JSON.stringify() 第三个参数可以通过指定缩进字符数(数字)或者 符号
let a = {
  b: 42,
  c: "42",
  d: [1,2,3]
}
JSON.stringify(a, null, 3);
// {
//    "b": 42,
//    "c": "42",
//    "d": [
//      1,
//       2,
//       3
//    ]
// } 
JSON.stringify(a, null, '-');
// {
// -"b": 42,
// -"c": "42",
// -"d": [
// --1,
// --2,
// --3
// -]
// }

9、假值列表

强制类型转换布尔类型为false,其他均为真值

  • undefined
  • null
  • false
  • +0, -0, NaN
  • ""

10、|| 和 &&

  • || 运算符 一般用来设置默认值
function foo(a, b){
  a = a || 'hello' ;
  b = b || 'world' ; 
} 
  • && 如果 左边的表达式满足条件,则执行右边的表达式,而并非返回boolean
a && b   =>  a ? b : a
a || b   =>  a ? a : b

11、== 和 ===

  • ===在进行比较相等中不允许进行强制类型转换,==允许
  • 不同类型之间进行比较像等(==)
  1. numberbooleanstring三种类型在进行比较的时候,会转换为数字进行判断
42 == '42'  // true
42 == true  // false
42 == false // false 
1 == true   // true
"1" == true // true

2.undefinednull

undefined == null       // true
undefined == undefined  // true
null == null            // true

3.object 和 数字( 或者字符串 ) object会调用toPrimitive抽象操作

"abc" == Object("abc")  //  true
42 == [42]              // true

特殊情况(nullundefined不能被封装为对象)

# Object(null)                     // {}
null == Object(null)               // false 
undefined == Object(undefined)     // false
  1. 实现一个函数满足以下情况
ifa == 2 && a == 3){
  // pass
}
var i = 2
Number.prototype.valueOf = function(){
  return i++
}
var a = new Number(42)
if(a == 2 && a == 3){
  console.log("'I'm here!'")
}

12、标签语句

  • continuebreak 在进行跳过或者中断循环的时候,通过标签语句可以对其进行限定范围
outerloop: for(var i = 0;i < 3; i++){
  console.log(i)
  for(var j = 0;j < 5;j++){
    if(j == 3){
      continue outerloop;
    }
    console.log(j)
  }
}
console.log('exit')
// 0 0 1 2 1 0 1 2 2 0 1 2 exit

13、[] 与 {} 求和

# [] 被强制转换为'' , {}被转换为[object Object]
[] + {} ; // '[object Object]'        
# {} 被识别为代码块,不做任何处理 ,[] 被强制转换为0
{} + [] ; // 0  

14、全局DOM变量

在创建带有id属性的DOM元素时也会创建同名的全局变量,所以尽量不要使用全局变量进行命名,避免冲突

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>
		<div id="div" ></div>
	</body>
	<script type="text/javascript">
		console.log(div)  //  HTML元素
	</script>
</html> 

15、script标签

  • 多个script标签加载的文件,是一个整体还是相互独立? 相互独立运行,共享全局变量
  • 对于前面运行的js中声明的变量,后面可以直接引用,
  • 同一个文件内部存在变量声明提前,script之间不存在 *script发生错误终止运行,但并不影响后续script中的代码
# ReferenceError: foo is not defined
<script>
  foo()
</script>
<script>
  function foo(){
    console.log('foo')
  }
</script>
  • 内联代码中不能出现</script>,一旦出现就会视为代码块结束。
<script>
  var code = "<script>alert('hello world!')</script>";
</script>

变通方法

<script>
  var code = "<script>alert('hello world!')</scr" + "ipt>";
</script>

16、保留字

Let this long package float,
Goto private class if short.
While protected with debugger case,
Continue volatile interface.
Instanceof super synchronized throw,
Extends final export throws.

Try import double enum?
False, boolean, abstract function,
Implements typeof transient break!
Void static, default do,
Switch int native new.
Else, delete null public var 
In return for const, true, char
... Finally catch byte.

你不知道的JavaScript(中卷) 提取码:8utt