你不知道的js(中卷)第一部分 读书笔记

第一章 类型

类型是值的内部特征,对于语言引擎和开发人员来说,数字42和字符串42采取不同的处理方式,那它们就是不同的类型。

1.1 类型

要正确合理的进行类型转换,我们必须掌握js中的各个类型及其内在行为。例如数字42作为string来处理,要获取其中的第二个字符2,就需要将number类型转换为string类型。
如何进行强制类型转换,需要我们来深入了解值与类型。

1.2 内置类型

js中有七种内置类型,除了object以外,其它的都是基本类型。

  1. null
  2. undefined
  3. boolean
  4. number
  5. string
  6. object
  7. symbol

类型检测:

  • typeof:它能够识别除了null以外的其它基本类型,而typeof null,数组,对象均为 object,typeof 方法为function
    ⚠️:函数方法属于object的一个子类型,也被称为可调用对象,同时它还拥有length属性
function a(b,c) {
  /* .. */
}
a.length; // 2 代表其声明的参数个数

1.3 值和类型

js中的变量是没有类型的,只有值才有。同时js不做类型强制,不会要求变量总是持有与其初始值同类型的值。

1.3.1 undefined 和 undeclared

变量声明了,但未持有值的时候为undefined,此时typeof也会返回undefined。
若变量在作用域中没有进行声明,则是undeclared。但用typeof检测会返回undefined。

var a;
a; // undefined
b; // ReferenceError: b is not defined
typeof a; // "undefined"
typeof b; // "undefined"

1.3.2 typeof Undeclared

因为typeof的安全防范机制(阻止报错),未声明的变量返回的值为undefined。这样处理是很有帮助的,因为多个脚本文件会在共享的全局命名空间中加载变量。

例如:在程序中使用全局变量DEBUG,而全局变量声明var DEBUG = true只在debug.js文件中才有,该文件在开发和测试环境会被加载到浏览器,生产环境不予加载。那如何在程序中检查全局变量不会出现ReferenceError错误呢?代码如下。

// 当DEBUG未被声明时,这样会抛出错误
if (DEBUG) {
 console.log( "Debugging is starting" );
}
// 这样是安全的
if (typeof DEBUG !== "undefined") {
 console.log( "Debugging is starting" );
}

同时对内建的API也有帮助,代码如下,比如要为某个缺失的功能写补充代码,一般不会用var atob来声明变量,如果在if中使用var atob,声明会被提升到作用域的最顶层,即使if条件不成立也会进行变量提升。在有些浏览器中,对于一些特殊的内建全局变量,重复声明会报错,因此去掉var可以防止声明被提升,

if (typeof atob === "undefined") {
 atob = function() { /*..*/ };
}

另外通过window也可以检查该变量是否是全局对象。但它存在局限,比如当代码需要运行在除了浏览器以外的其它js环境中时,如服务器端,node.js等,此时的全局对象就并非是window。

第二章 值

2.1 数组

数组可以容纳任何类型的值,当数组声明后就可向其中加入值,不需要预先设定大小。使用delete运算符删除数组元素时,删除后数组的length属性不变

let a = [1,2,3];
delete a[0];
console.log(a); // (3) [empty, 2, 3]
console.log(a.length); // 3
console.log(typeof a[0]) // undefined 

数组可以通过数字进行索引,但有趣的是它们也是对象,也可以包含字符串键值和属性。

var a = [ ];
a[0] = 1;
a["foobar"] = 2;
a.length; // 1
a["foobar"]; // 2
a.foobar; // 2

而当字符串键值能够被强制转换成十进制数字,它就会被当作是数字索引进行处理。

var a = [ ];
a["13"] = 42;
a.length; // 14

⚠️:在数组中加入字符串键值 / 属性并不是一个好主意。因此还是要用对象进行存放。

2.1.1 类数组

定义:只包含使用从零开始,且自然递增的整数做键名,并且定义了length表示元素个数的对象,我们就认为它是类数组对象,下面代码的arrayLike就是类数组对象

var array = ['zhangsan', 'lisi', 'wangwu'];

var arrayLike = {
    0: 'zhangsan',
    1: 'lisi',
    2: 'wangwu',
    length: 3
}

2.2 字符串

字符串经常被当成字符数组,且字符串和数组都是类数组,有length属性和indexof、concat方法。

字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串,而数组的成员函数都是在其原始值的基础上进行操作。

我们可以采取join(“”)将字符数组转换为字符串。

2.3 数字

2.3.1 数字的语法

js的数字常量一般用十进制表示,且数字前面的0和小数点后面的0都可以省略,但这种书写方式不建议。

特别大和特别小的数字默认用指数格式显示,与toExponential() 函数的输出结果相同。

var a = 5E10;
a; // 50000000000
a.toExponential(); // "5e+10"
var b = a * a;
b; // 2.5e+21
var c = 1 / a;
c; // 2e-11
  • tofixed方法可指定小数部分的显示位数
    这里需要注意运算符 . ,因为它是一个有效的数字字符,会被优先识别为数字常量的一部分。
// 无效语法:
42.toFixed( 3 ); // SyntaxError
// 下面的语法都有效:
(42).toFixed( 3 ); // "42.000"
0.42.toFixed( 3 ); // "0.420"
42..toFixed( 3 ); // "42.000"
42 .toFixed(3); // "42.000"  注意42后面有一个空格
  • toPrecision方法用来指定有效数位的显示位数
a.toPrecision( 1 ); // "4e+1"
a.toPrecision( 2 ); // "43"
a.toPrecision( 3 ); // "42.6"
a.toPrecision( 4 ); // "42.59"
a.toPrecision( 5 ); // "42.590"
a.toPrecision( 6 ); // "42.5900"

2.3.2 较小的数值

二进制浮点数的问题会出现 0.1 + 0.2 === 0.3 为false的情况。
我们可以通过设置一个误差范围值的方式来解决上述问题,对js而言,这个值为2^-52。

let a = 0.1 + 0.2 - 0.3
a < Math.pow(2, -52) // true

因此可以设置一个函数,当两式相减,它的值会小于拟定的误差范围值,这样就默认两个数字相等。

2.3.3 整数的安全范围

整数的最大值是2^53 -1,即 9007199254740991,最小值为- 9007199254740991。

2.3.4 整数检测

  • Number.isInteger方法
  • Number.isSafeInteger方法,用来检查是否是安全的整数

2.3.5 32位有符号整数

a | 0 可以将变量a中的数值转换为32位有符号整数

let b = Math.pow(2, 31) -1 
let c = b | 0;
console.log(c) // 2147483647
let b = Math.pow(2, 31)
let c = b | 0;
console.log(c) // -2147483648

2.4 特殊数值

2.4.1 不是值的值

  • undefined 指没有值
  • null 指空值
  • null是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。但是undefined确是一个标识符,可以被当作变量来使用和赋值,但是不建议这样做。

2.4.2 特殊的数字

  • NaN: 不是一个数字, NaN !== NaN。
    我们可以通过isNaN来判断一个值是否是NaN。但存在一个严重的缺陷,如var b = "foo";window.isNaN( b ); // true
    后面ES6提出 Number.isNaN来判断。推荐使用
  • 无穷数:Infinity 和 - Infinity。
var a = 1 / 0;  // Infinity
var b = Infinity / Infinity; // NaN
var c = 1 / Infinity; // 0
var d = -1 / Infinity; // -0
  • 零数:+0 和 -0
var a = 0 / -3; // -0
var b = 0 * -3; // -0
// 加减运算不会得到-0
// 但是规范定义的返回结果是这样!
a.toString(); // "0"
a + ""; // "0"
String( a ); // "0"
JSON.stringify( a ); // "0"
// 如果反过来将其从字符串转换为数字,得到的结果是准确的
+"-0"; // -0
Number( "-0" ); // -0
JSON.parse( "-0" ); // -0
-0 === 0 // true

2.4.3 特殊等式

Object.is 来判断两个值是否绝对相等。

var a = 2 / "foo";
var b = -3 * 0;
Object.is( a, NaN ); // true
Object.is( b, -0 ); // true
Object.is( b, 0 ); // false

⚠️:能使用== 和 === 时,就尽量不要使用Object.is,因为前者的效率更高。

2.5 值和引用

var a = 2;
var b = a; // b是a的值的一个副本
b++;
a; // 2
b; // 3
var c = [1,2,3];
var d = c; // d是[1,2,3]的一个引用
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]
  • 基本类型值总是通过值复制的方式来赋值/传递,包括null、undefined、string、number、boolean和symbol。
  • 复合值如对象和函数,总是通过引用复制的方式来赋值/传递。
    上述代码中,变量a是持有该值的一个复本,b持有它的另一个复本,两个为不同的值,因此改变其中一个,另一个不受影响。而c和d分别指向同一个复合值的两个不同的引用,只要其中一个变量发生改变(如调用push方法),会影响另一个变量。
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,b指向不同的地址,所以a不会受影响。

第3章 原生函数

常见的原生函数:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()

3.1 内部属性[[Class]]

所有typeof返回值为object的对象都包含一个内部属性[[Class]],它一般通过Object.prototype.toString()来查看。

Object.prototype.toString.call( [1,2,3] );
// "[object Array]"
Object.prototype.toString.call( /regex-literal/i );
// "[object RegExp]"

数组的内部属性值为Array,正则表达式的值为RegExp,多数情况下,对象的内部属性和创建该对象的原生函数相对应,但也有特例。Null()和Undefined()这样的原生函数不存在,但是属性值为Null和Undefined。

Object.prototype.toString.call( null );
// "[object Null]"
Object.prototype.toString.call( undefined );
// "[object Undefined]"

3.2 封装对象包装

由于基本类型值没有length和toString这样的属性和方法,需要通过封装对象才能访问,此时js会自动为基本类型值包装一个封装对象。
使用封装对象,有些地方需要特别注意

var a = new Boolean( false );
if (!a) { 
 console.log( "Oops" ); // 对象为真值,因此执行不到这里
}

如果想要自动封装基本类型值,可以使用Object函数,但不推荐。

3.3 拆封

想要得到封装对象中的基本类型值,可以使用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

在需要用到封装对象中的基本类型值的地方会发生隐式拆封

var a = new String( "abc" );
var b = a + ""; // b的值为"abc"
typeof a; // "object"
typeof b; // "string"

3.4 原生函数作为构造函数

3.4.1 Array()

不要求必须带new关键字,不带时,会自动补上。当Array构造函数只带一个数字参数时,该参数会被作为数组的预设长度,而非只充当数组中的一个元素。
⚠️:永远不要创建和使用空单元数组

3.4.2 Object(…)、Function(…) 和 RegExp(…)

除非万不得已,否则尽量不要使用这些原生函数
对于正则而言,强烈建议使用常量形式来定义正则表达式,这样不仅语法简单,执行效率也更高,因为js引擎在代码执行前会对它们进行预编译和缓存。与前面的构造函数不同,RegExp(…) 有时还是很有用的,比如动态定义正则表达式时

var name = "Kyle";
var namePattern = new RegExp( "\\b(?:" + name + ")+\\b", "ig" );
var matches = someText.match( namePattern );

3.4.3 Date() 和 Error()

  • Date: 用于创建日期对象,必须使用new Date(),它可以携带参数,用来指定日期和时间,而不带参数的话则使用当前日期和时间。
    • getTime方法以及Date.now()获取当前的时间戳
  • Error:主要是为了获得当前运行栈的上下文,该栈的上下文信息包括函数调用栈信息和产生错误的代码行号,便于调试。它带不带new关键字都可。错误对象通常与throw一起使用:throw new Error( "x wasn’t provided" );

3.4.4 原生原型

  • String.prototype.indexOf(): 在字符串中找到指定子字符串的位置
  • String.prototype.charAt(): 获得字符串指定位置上的字符
  • String.prototype.substr()、substring()、slice(): 获取字符串的指定部分
  • String.prototype.toUpperCase()、toLowerCase():将字符串转换为大写或小写
  • String.prototype.trim(): 去掉字符串前后的空格,返回一个新的字符串
    上述方法并不改变原字符串的值,而是返回一个新字符串。

第4章 强制类型转换

4.1 值类型转换

js中的强制类型转换总是返回标量基本类型值。将值从一种类型转换为另一种类型,称其为类型转换,这是显示的情况,隐式的情况称为强制类型转换。

var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String( a ); // 显式强制类型转换

4.2 抽象值操作

4.2.1 ToString

ToString: 负责处理非字符串到字符串的强制类型转换

  • 基本类型值的字符串化规则为:null 转换为 “null”,undefined 转换为 “undefined”,true
    转换为 “true”。数字的字符串化则遵循通用规则,不过极小极大的数字使用指数形式。
  • 对普通对象来说,除非自行定义,否则 toString()(Object.prototype.toString())返回
    内部属性 [[Class]] 的值,如 “[object Object]”。
  • 数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用 “,” 连接起

    JSON字符串化:JSON.stringify(…)
    安全的JSON值是指能呈现有效的JSON格式的值,它都可以使用JSON.stringify(…) 字符串化。而在对象中遇到undefined,function,symbol时会自动将其忽略,在数组中则会返回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}"
  1. 向JSON.stringify()传递一个可选参数replacer,它可以是数组或函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。
    • replacer是数组,则必须是一个字符串数组,其中包含序列化要处理的对象属性名称,除此之外其他的属性则被忽略。
    • replacer是一个函数,它会对对象本身调用一次,然后对对象中的每个属性各调用一次,每次传递两个参数,键和值。如果要忽略某个键就返回undefined,否则返回指定的值。
var 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 !== "c") return v;
} );
// "{"b":42,"d":[1,2,3]}"
  1. 可选参数space,用来指定输出的缩进格式
    • space为正整数,指定每一级缩进的字符数
    • space为字符串,此时最前面的十个字符被用于每一级的缩进,只取前10

4.2.2 ToNumber

Number(true); // 1
Number(false); // 0
Number(undefined); // NaN
Number(null); // 0

对象(包括数组)会首先被转换为基本类型值,如果返回的是非数字的基本类型值,则会强制转换为数字。
为了将值转换为基本类型值,首先检查是否有valueOf方法,如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用toString的返回值来进行强制类型转换,均不返回基本类型值,则产生TypeError错误。

var a = {
 valueOf: function(){
 return "42";
 }
};
var b = {
 toString: function(){
 return "42";
 }
};
var c = [4,2];
c.toString = function(){
 return this.join( "" ); // "42"
};
Number( a ); // 42
Number( b ); // 42
Number( c ); // 42
Number( "" ); // 0
Number( [] ); // 0
Number( [ "abc" ] ); // NaN

Number( [ "abc" ] ); // NaN: 首先检查数组的ValueO方法,有但是它返回的是一个数组对象本身,不是基本类型值,因此会再去检查数组的toString方法,得到字符串’abc’,最终该字符串转换为数字为NaN,所以结果为NaN。同样空数组的toString方法,得到空字符串,转换为数字为0。

4.2.3 ToBoolean

记住目前的假值

  • undefined
  • null
  • false
  • +0,-0和NaN
  • “”
    除此之外,其它值都是真值。

4.3 显示强制类型转换

4.3.1 字符串和数字之间的显示转换

字符串和数字之间的转换是通过String()和Number()这两个原生函数实现的,前面没有new关键字并不会创建封装对象。

4.3.2 显示解析数字字符串

解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。但解析和转换两者之间有明显差别。

  • 解析允许字符串中含有非数字字符,解析从左到右进行,遇到非数字字符就停止。
  • 转换不允许出现非数字字符,否则会失败并返回NaN。
    解析字符串中的浮点数可以使用parseFloat函数。⚠️ parseInt针对的是字符串值,向其传递数字和其它类型如true、function、[1,2,3]是没用的。非字符串参数会首先被强制转换为字符串
parseInt( 1/0, 19 ); // 18
parseInt( 0.000008 ); // 0 ("0" 来自于 "0.000008")
parseInt( 0.0000008 ); // 8 ("8" 来自于 "8e-7")
parseInt( false, 16 ); // 250 ("fa" 来自于 "false")
parseInt( parseInt, 16 ); // 15 ("f" 来自于 "function..")
parseInt( "0x10" ); // 16
parseInt( "103", 2 ); // 2

分析: parseInt( 1/0, 19 );首先1/0 的结果是Infinity,它属于数字会被转化为字符串,因此对应解析的就是parseInt( ‘Infinity’, 19 );,而19此时表示的就是进制数,对应的范围是0-9,a-i。第一个字符为I,解析为18而第二个字符n不会有效数字字符,就停止解析,最终结果就是18。

4.3.3 显示转换为布尔值

var a = "0";
var b = [];
var c = {};
var d = "";
var e = 0;
var f = null;
var g;
Boolean( a ); // true
Boolean( b ); // true
Boolean( c ); // true
Boolean( d ); // false
Boolean( e ); // false
Boolean( f ); // false
Boolean( g ); // false

4.4 隐式强制类型转换

作用: 减少冗余,让代码更简洁

4.4.1 字符串和数字之间的隐式强制类型转换

var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"
// a,b均为数组,会通过toString()方法转换为字符串分别为'1,2'和'3,4',最终结果就是两字符串的拼接'1,23,4'
[] + {} // "[object Object]"
// []通过toString()转换为"",{}转换为"[object Object]",两个字符串再拼接
{} + [] // 0
// {}会被看成代码块执行,{} + [] 相当于 + [],它是一个隐形转换,会将字符串转换为数字,最终输出0。
  • 数字和字符串相加会转换为字符串
  • 减号-可以将字符串强制类型转换为数字

4.5 宽松相等和严格相等

== 允许在想等比较中进行强制类型转换,而 ===不允许

  • 字符串和数字之间的相等比较,在宽松相等条件下,会将字符串转换为数字
  • 其它类型和布尔值之间的相等比较,在宽松相等条件下,会先将布尔值转换为数字,再与其它类型进行比较。
  • null 和 undefined 宽松相等
  • 对象和非对象之间的相等比较
    1. 如果x是字符串或数字,y是对象,对象会先调用valueOf方法,如果有值且为标量基本类型,则会和字符串或数字进行接下来的比较,否则会调用toString方法,如果有值且为标量基本类型,则会和字符串或数字进行接下来的比较。
    2. 代码如下:因为没有对应的封装对象,所以 null 和 undefined 不能够被封装(boxed),Object(null)和 Object() 均返回一个常规对象。
      NaN 能够被封装为数字封装对象,但拆封之后 NaN == NaN 返回 false,因为 NaN 不等于 NaN
var a = null;
var b = Object( a ); // 和Object()一样
a == b; // false
var c = undefined; 
var d = Object( c ); // 和Object()一样
c == d; // false
var e = NaN; 
var f = Object( e ); // 和new Number( e )一样
e == f; // false

4.6 抽象关系比较

var a = [ 42 ];
var b = [ "43" ];
a < b // a、b会通过toString()方法转换为字符串,再进行比较
var a = { b: 42 };
var b = { b: 43 };
a < b; // false
a == b; // false
a > b; // false
a <= b; // true
a >= b; // true
// 当比较<和>时,a、b会通过toString()方法都转化为字符串'[object Object]',再进行比较,自然为false。
// 而==的时候,不会进行强制类型转换,a,b对应的指针指向的地址不同,即使值一样也不会相等。
// a <= b 会转换为 !(a > b),因为a>b为false,自然 !false为true;a>=b同理。

第5章 语法

5.1 语句和表达式

match方法用于匹配字符串中是否含有正则表达式,没有返回null,有则返回该数组

let str =  "Hello World"
str.match( /[aeiou]/g ); //  ["e","o","o"]

5.2 运算符的优先级

优先级关系: && > || > ?:

  • a && b:若a为真,则最终的结果由b的值决定;若a为假,则没必要再判断b的值,a即代表最终的结果。
  • a || b: 若a为真,即代表最终的结果;若a为假,则最终的结果由b的值决定。

5.3 自动分号

5.4 错误

js中不仅有各种类型的运行时错误如TypeError,ReferenceError,SyntaxError等,它的语法中也定义了一些编译时错误。
早期错误:在编译阶段发现的代码错误,而语法错误是早期错误的一种,同时语法正确但不符合语法规则的情况也存在。这些错误在代码执行之前无法通过try…catch来捕获,相反,会导致解析/编译失败。
在ES5规范的严格模式定义了跟多早期错误,如函数参数不能重名,对象常量不能包含多个同名属性。
TDZ(暂时性死区):由于代码中的变量还没有初始化而不能被引用的情况。
如let块级作用域,在变量初始化之前使用变量就会产生错误。对未声明变量使用typeof不会产生错误,但是在TDZ中却会报错。

5.5 函数参数

5.6 try…finally

finally中的代码总是在try之后执行,如果try中出现return语句,那么调用该函数并得到返回值的代码也是在finnally之前执行还是之后执行呢?

function foo() {
 try {
 	return 42;
 } 
 finally {
 	console.log( "Hello" );
 }
 console.log( "never runs" );
}
console.log( foo() );
// Hello
// 42

这里的return 42先执行,并将foo()函数的返回值设置为42,然后try执行完毕,接着执行finally。最后foo函数执行完毕,console.log( foo() );显示返回42。
如果finally中抛出异常,函数就会在此处终止,就算try中已经有return设置了返回值,则该值会被丢弃。
finally 中的 return 会覆盖 try 和 catch 中 return 的返回值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值