JavaScript中的封装对象与类型

本篇文章是根据阅读《你不知道的JavaScript 中卷》个人总结所得,如若文中的部分观点有不恰当中出还请在评论区中指出😃

类型

JavaScript中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。

在JavaScript中共有中内置类型。

  • 空值null
  • 未定义undefined
  • 布尔值boolean
  • 数字number
  • 字符串string
  • 对象object
  • 符号symbol(es6新增)
 typeof undefined === "undefined"; // true
 typeof true === "boolean";   // true
 typeof 42 === "number";    // true
 typeof "42" === "string";    // true
 typeof { life: 42 } === "object";    // true
 // ES6中新加入的类型
 typeof Symbol() === "symbol";    // true

除对象外,其它统称基本类型。

在基本类型中除了null以外,都可以利用typeof运算符进行值的类型查看(typeof的返回值是字符串),

而对象(object)有些许特殊,只要是对象,利用typeof进行值的类型判断时都会返回"object"(function除外),在JavaScript中对象有很多(Atrray,Function,Date,Error等),无法对其进行准确的判断,此时可以使用Object原型对象上的toString方法去进行类型的准确判断(基本数据类型也可使用次方法判断出数据类型)。

 let val = function name(params) { };
 // 堪称万能方法!
 // 这个方法也可以将null类型准确的判断出来
 let str = Object.prototype.toString.call(val); // "[object Function]"
 let typeString = str.substring(8, str.length - 1); // "Function"

特殊的null

JavaScript中typeof null返回"object"的行为是一个历史遗留问题,它存在于JavaScript的早期版本中,并且由于兼容性原因一直保留至今,在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。尽管这看起来不符合直觉,但这个行为已经被定义为标准,因此在大多数现代JavaScript引擎中仍然如此。

     let val = null;
     // 这个方法也可以将null类型准确的判断出来
     let str = Object.prototype.toString.call(val); // "[object Null]"
     let typeString = str.substring(8, str.length - 1); // "Null"

数组

和其他强类型语言不同,在JavaScript中,数组可以容纳任何类型的值,可以是字符串、数字、对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的)。

     let a = [1, "2", [3]];
     a.length;       // 3
     a[0] === 1;     // true
     a[2][0] === 3;  // true
稀疏数组

含有空白或空缺单元的数组称之为稀疏数组。

     let a = []; // 稀疏数组
     a[0] = 1;
     console.log(a); // [1]
     console.log(a[2]); // undefined 

a[1]的值为undefined,但这与将其显式赋值为undefined(a[1] = undefined)还是有所区别


数组通过数字进行索引,但它们也是对象,所以也可以包含字符串键值和属性(但这些并不计算在数组长度内

     let a = [];
     a[0] = 1;
     a["foobar"] = 2;
     // 除了数字索引外,不更改数组中length属性的属性值。
     console.log(a.length); // 1
     console.log(a["foobar"]); // 2
     console.log(a.foobar); // 2

如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当作数字索引来处理。

     let a = [];
     a["13"] = 42; // 不推荐
     console.log(a.length); // 14

字符串

字符串和数组的确很相似,它们都是类数组,都有length属性以及indexOf(..)(从ES5开始数组支持此方法)和concat(..)方法(数组合并),JavaScript中字符串不可变的,而数组可变的。

     let str= "syh"
     // 可以通过下标的方式访问字符
     console.log(str[0]); // "s"
     // JavaScript中字符串是不可变的
     str[1] = "M"; // 无法修改!!
     console.log(str); // "syh"

访问字符串中字符的正确姿势😉。

let str= "syh"
console.log(str.charAt(0)); // “s”

数字

四舍五入?

tofixed(..)方法可指定小数部分的显示位数,输出结果是给定数字的字符串形式,如果指定的小数部分的显示位数多于实际位数就用0补齐

    let a = 42.59;
    a.toFixed(0); // "43"
    a.toFixed(1); // "42.6"
    a.toFixed(2); // "42.59"
    a.toFixed(3); // "42.590"
    a.toFixed(4); // "42.5900"

😱误区:😱

a.toFixed(1); // "42.6"

看似进行了四舍五入,实则并非如此!!toFixed() 方法遵循四舍六入五取偶的规则。 规则是:

  • 舍去位的数值< 5时直接舍去

  • 当舍去位的数值>= 6时,在舍去的同时向前进一位

  • 当舍去位的数值 = 5时:

    5后不为空且不全为0,在舍去的同时向前进一位 5后为空或全为0:

    • 5前数值为奇数,则在舍去的同时向前进一位
    • 5前数值为偶数,则直接舍去
   let num1 = 0.865;
   // 舍去位为 5 
   console.log(num1.toFixed(2)); // "0.86" 
   let num2 = 0.8675;
   // 舍去位为 7
   console.log(num1.toFixed(2)); // "0.87"

如若有四舍五入的需求扔需使用 Math.round()这个专业的方法,这个方法返回的值的类型是number类型,而非toFixed方法返回的string类型。

    let num1 = 0.865;
    console.log(Math.round(num1 * 100) / 100); // 0.87 ==> number 类型
    console.log(typeof (Math.round(num1 * 100) / 100)); // "number"
数字字面量与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"
二进制、八进制、十六进制
    0o363;      // 243的八进制
    0O363;      // 同上

    0b11110011; // 243的二进制
    0B11110011; // 同上

    0xf3; // 243的十六进制
    0Xf3; // 同上

考虑到代码的易读性,不推荐使用0O363格式,因为0和大写字母O在一起容易混淆。建议尽量使用小写的0x、0b和0o

undefined与null

undefined类型只有一个值,即undefined。null类型也只有一个值,即null。它们的名称既是类型也是值。

  • null指空值(empty value)
  • undefined指没有值(missing value)

或者:

  • undefined指从未赋值
  • null指曾赋过值,但是目前没有值

null是一个特殊关键字不是标识符,我们不能将其当作变量来使用和赋值。然而undefined一个标识符可以被当作变量来使用和赋值(及其不建议,因为代码会变得非常糟糕!)。

    function foo() {
    //  为全局作用域下的 undefined 赋值
      undefined = 2; // 非常糟糕的做法!
    }
    foo();
    // 严格模式下 全局作用域下的 undefined 不允许被修改,它是只读属性
    function foo() {
      "use strict";
      undefined = 2; // TypeError! Cannot assign to read only property 'undefined' of object
    }
    foo();

非严格和严格两种模式下,我们可以声明一个名为undefined的局部变量。再次强调最好不要这样做!

    function foo() {
      "use strict";
      var undefined = 2;
      console.log(undefined); // 2
    }
    foo();
void运算符

通过void运算符即可得到undefined。

按惯例我们用void 0来获得undefined(这主要源自C语言,当然使用void true或其他void表达式也是可以的)。void 0、void 1和undefined之间并没有实质上的区别

NaN

NaN意指“不是一个数字”(not a number),当无法返回一个有效的数字时,这种情况下返回值为NaN,代表着执行数学运算没有成功。可以将它理解为“无效数值”“失败数值”或者“坏数值”。

    let a = 2 / "foo";      // NaN
    typeof a === "number";  // true

“不是数字的数字”仍然是数字类型。

特殊性

NaN是一个特殊值,它和自身不相等。

    let a = 2 / "foo";
    a == NaN;   // false
    a === NaN;  // false

可以使用内置的全局工具函数isNaN(..)来判断一个值是否是NaN

注意:isNaN(…)方法只有接受的参数是一个number类型才会得到“合理”的结果。

例如:

    let a = 2 / "foo"; // 带有强制类型转换, "foo" 会被强制类型转换为 number 类型
    console.log(Number("foo")); // NaN
    // 实际为 var = 2 / NaN ;
    let b = "foo";
    a; // NaN
    b; "foo"
    window.isNaN(a); // true
    // 给 isNaN() 传入了 string 类型,而非 number 类型
    window.isNaN(b); // true 

💕最稳妥的方法: 💕

ECMAScript 2015 中定义的 Number.isNaN() 来判断。它是原来的全局 isNaN() 的更稳妥的版本。

    let a = 2 / "foo"; // 带有强制类型转换 "foo" 会被强制类型转换为 number 类型
    let b = "foo";
    console.log(Number.isNaN(a)); // true
    console.log(Number.isNaN(b)); // false 

+0 与 -0

JavaScript有一个常规的0(也叫作+0)和一个-0

对负零进行字符串化会返回"0"

    let a = 0 / -3;
    // 至少在某些浏览器的控制台中显示是正确的
    console.log(a);   // -0
    // 但是规范定义的返回结果是这样!
    console.log(a.toString());   // "0"
    console.log(a + ""); // "0"
    console.log(String(a)); // "0"
    // JSON也如此
    console.log(JSON.stringify(a)); // "0"

字符串 "-0"转换为number 类型会将其转换为 number 类型的 -0

注意: JSON.stringify(-0)返回"0",而JSON.parse("-0")返回-0

+0 与 -0的比较操作
    -0 == 0;  // true
    -0 === 0; // true
    0 > -0;   // false
    0 < -0;   // false

无论是宽松相等还是严格相等,+0 与 -0 都相等。


对于 NaN 不等于 NaN+0 等于 -0这种情况,在ES6中新加入了一个工具方法Object.is(..)来判断两个值是否绝对相等(严格相等)。

    let a = 2 / "foo"; // NaN
    let b = -30; // -0
    Object.is(a, NaN);  // true
    Object.is(b, -0);   // true
    Object.is(b, 0);    // false

能使用 =====时应避免使用 Object.is() 方法,因为前者效率更高。

原生函数

JavaScript的内建函数,也叫原生函数。

常用的原生函数有: • String() • Number() • Boolean() • Array() • Object() • Function() • RegExp() • Date() • Error() • Symbol()——ES6中新加入的!

原生函数可以被当作构造函数来使用

    let a = new String("abc");
    typeof a;                             // 是"object",不是"String"
    // 原型链判断
    a instanceof String;                 // true
    Object.prototype.toString.call(a); // "[object String]"

通过构造函数(如new String(“abc”))创建出来的是封装了基本类型值(如"abc")的封装对象

内部属性[[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]"

上例中,数组的内部[[Class]]属性值是"Array",正则表达式的值是"RegExp"。多数情况下,对象的内部[[Class]]属性和创建该对象的内建原生构造函数相对应,但并非总是如此。


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

虽然Null()Undefined()这样的原生构造函数并不存在,但是内部[[Class]]属性值仍然是"Null""Undefined"


    Object.prototype.toString.call("abc");
    // "[object String]"
    Object.prototype.toString.call(42);
    // "[object Number]"
    Object.prototype.toString.call(true);
    // "[object Boolean]"

基本类型值被各自的封装对象自动包装,所以它们的内部[[Class]]属性值分为"String"、“Number"和"Boolean”

封装对象包装

由于基本类型值没有.length.toString()这样的属性和方法,需要通过封装对象才能访问,此时JavaScript自动为基本类型值包装(box或者wrap)一个封装对象。

    let a = "abc";
    a.length; // 3
    a.toUpperCase(); // "ABC"

    let a = null;
    let b = Object(a); // 和Object()一样 ---> 空对象

    let c = undefined;
    let d = Object(c); // 和Object()一样 ---> 空对象

因为没有对应的封装对象,所以null和undefined不能够被封装(boxed), Object(null)和Object()均返回一个常规对象(空对象)。

拆封

如果想要得到封装对象中的基本类型值,可以使用valueOf()函数

    let a = new String("abc");
    let b = new Number(42);
    let c = new Boolean(true);
    a.valueOf(); // "abc"
    b.valueOf(); // 42
    c.valueOf(); // true

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

    let a = new String("abc");
    // a 会进行 ToPrimitive 的强制类型转换
    let b = a + ""; // b的值为"abc"
    typeof a;       // "object"
    typeof b;       // "string"

原生函数作为构造函数

Array(…)

构造函数Array(…)不要求必须带new关键字。不带时,它会被自动补上。因此Array(1,2,3)和new Array(1,2,3)的效果是一样的。

Array构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度(length),而非只充当数组中的一个元素。这样创建出来的只是一个空数组,只不过它的length属性被设置成了指定的值。

    let a = new Array(3);
    console.log(a.length); // 3
    console.log(a); //  (3)[空属性 x 3]
    console.log(a[0]); // undefined
    // 这意味着它有三个值为undefined的单元,但实际上单元并不存在
RegExp(…)

建议使用常量形式(如/^a*b+/g)来定义正则表达式,这样不仅语法简单,执行效率也更高,因为JavaScript引擎在代码执行前对它们进行预编译缓存

    let name = "Kyle";
    let namePattern = new RegExp("\b(? :" + name + ")+\b", "ig");
Date(…)

创建日期对象必须使用new Date()。其可以带参数,用来指定日期和时间,而不带参数的话则使用当前的日期和时间。如果调用Date()时不带new关键字,则会得到当前日期的字符串值。

    const date = new Date();
    console.log(date);
    console.log(typeof(date)); // "object"
    console.log(Date());
    console.log(typeof(Date())); // "string"

image-20230905222557995.png

强制类型转换

将值从一种类型转换为另一种类型

抽象操作ToString

处理非字符串到字符串的强制类型转换。

基本类型值的字符串化规则为:null转换为"null", undefined转换为"undefined", true转换为"true"。

普通对象来说,除非自行定义(部分对象会对toString方法进行重写),否则toString()(Object.prototype.toString())返回内部属性[[Class]]的值,如"[object Object]"

ES6允许符号到字符串显式强制类型转换,然而隐式强制类型转换会产生错误

    let s1 = Symbol("cool");
    String(s1); // "Symbol(cool)"

    let s2 = Symbol("not cool");
    s2 + "";  // TypeError
JSON字符串化

JSON.stringify(…)在将JSON对象序列化为字符串时也用到了ToString,序列化的结果总是字符串😂

JSON.stringify(..)并不是强制类型转换。

    JSON.stringify(42);   // "42"
    JSON.stringify("42"); // ""42""(含有双引号的字符串)
    JSON.stringify(null); // "null"
    JSON.stringify(true); // "true"

被JSON.stringify(…)所忽略的值:

对象中遇到undefinedfunctionsymbol会自动将其忽略,在数组中则会返回null(以保证单元位置不变)

    JSON.stringify(undefined);  // undefined
    JSON.stringify(function () { }); // undefined
    JSON.stringify(Symbol("syh")); // undefined
    // ---------------------------------------------------------------------
    JSON.stringify(
      [1, undefined, function () { }, 4, Symbol("syh")]
    );  // "[1,null,null,4,null]"
    JSON.stringify(
      { a: 2, b: function () { }, c: Symbol("syh"), d: undefined }
    );  // '{"a":2}'

这也是为什么在深拷贝时,JSON.parse(JSON.stringify())方式的不保险的原因所在。如果是类型已知且确定没有undefined、function和symbol时,使用这种方式处理才比较保险。

JSON.stringify(…)的可选参数replacer
replacer是一个数组

如果replacer是一个数组,那么它必须是一个字符串数组,其中包含序列化要处理的对象的属性名称,除此之外其他的属性则被忽略

     let a = {
       b: 42,
       c: "42",
       d: [1, 2, 3]
     };
     // 只对属性 b、c 进行序列化,忽略 d 属性
     console.log(JSON.stringify(a, ["b", "c"])); // '{"b":42, "c":"42"}'
replacer是一个函数

如果replacer是一个函数,它会对对象本身调用一次然后对象中每个属性各调用一次,每次传递两个参数,,如果要忽略某个键就返回undefined,否则返回指定的值。

     let a = {
       b: 42,
       c: "42",
       d: [1, 2, 3]
     };
     JSON.stringify(a, function (k, v) {
       // k ==> 属性名
       // v ==> 属性值
       if (k !== "c") return v;
     });
     // '{"b":42, "d":[1,2,3]}'

第一次调用 JSON.stringify 时(就是对对象本身调用的那次),传递给 replacer 函数的参数 kundefined,而参数 v 则是要序列化的整个对象 a。这是因为在第一次调用 replacer 函数时,它处理的是最外层的对象。


由于JSON 字符串化是递归的😎,这意味着它会递归地处理嵌套在对象或数组中的其他对象和数组,以将整个数据结构转换为 JSON 字符串,因此数组[1,2,3]中的每个元素都会通过参数v传递给replacer,即1、2和3,参数k是它们的索引值,即0、1和2。


抽象操作ToNumber

将非数字值当作数字来使用

其中true转换为1, false转换为0undefined转换为NaN, null转换为0

“”、“\n”(或者" "等其他空格组合)等空字符串被ToNumber强制类型转换为0。

抽象操作ToPrimitive

对象(包括数组)会首先被转换为相应的基本类型值,而转换为相应基本类型值的方法便是抽象操作ToPrimitive

ToPrimitive步骤:

首先(通过内部操作[[DefaultValue]])检查该值是否有valueOf()方法(封装对象的拆封操作)。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用toString()的返回值(如果存在)来进行强制类型转换。

如果valueOf()和toString()均不返回基本类型值,会产生TypeError错误。

从ES5开始,使用Object.create(null)创建的对象(“真空对象”)[[Prototype]]属性为null,并且没有valueOf()和toString()方法,因此无法进行强制类型转换

     let a = {
       valueOf: function () {
         return "42";
       }
     };let b = {
       toString: function () {
         return "42";
       }
     };let c = [4, 2];
     c.toString = function () {
       // join("") 中空字符串相当于 ==> "4" + "2"
       //join("-")  ==> "4-2"
       return this.join(""); // "42"
     };
     // 调用 valueOf() 方法
     Number(a);  // 42
     // 首先调用 valueOf() 方法,发现没有,则调用 toString() 方法
     Number(b);  // 42
     // 重写了数组本身的 toString 方法
     Number(c);  // 42
     Number(""); // 0
     // "" 强转 ==> 0
     Number([]); // 0
     // 调用数组的 toString 方法 ==> "abc" 
     // 对 "abc" 进行 number 类型强转 ==> NaN
     Number(["abc"]); // NaN

注意: Array 本身没有 valueOf 方法。

    Array(1,23).__proto__.hasOwnProperty("valueOf"); // false

hasOwnProperty() 方法不对原型链进行检查,只会检查对象自身的属性。

in 操作符可以用于检查属性是否存在于对象本身或其原型链中。

如果只想检查对象自身是否具有属性,使用 hasOwnProperty() 方法是更合适的。如果希望检查对象的整个原型链,包括继承的属性,使用 in 操作符。

ToBoolean

被强制类型转换为布尔值。

假值

•undefined • null • false • +0、-0和NaN • “”

假值列表以外的值都是真值。

字符串和数字之间的转换

字符串和数字之间的转换是通过String(..)Number(..)这两个内建函数来实现的,请注意它们前面没有new关键字并不创建封装对象

    let a = 42;
    let b = String(a);

    let c = "3.14";
    let d = Number(c);

    b; // "42"
    d; // 3.14

a.toString()是显式的,不过其中涉及隐式转换。因为toString()对42这样的基本类型值不适用,所以JavaScript引擎会自动为42创建一个封装对象,然后对该对象调用toString()。这里显式转换中含有隐式转换。

    let a = 42;
    let b = a.toString();

    b; // "42"

    let c = "3.14";
    let d = +c;

    d; // 3.14

+c+运算符的一元(unary)形式(即只有一个操作数)。+运算符显式地将c转换为数字,而非数字加法运算。

显式转换为布尔值

    let a = "0";
    let b = [];
    let c = {};

    let d = "";
    let e = 0;
    let f = null;
    let g;

    Boolean(a); // true
    Boolean(b); // true
    Boolean(c); // true

    Boolean(d); // false
    Boolean(e); // false
    Boolean(f); // false
    Boolean(g); // false

方法二:

显式强制类型转换为布尔值最常用的方法是!!,因为第二个!会将结果反转回原值

    let a = "0";
    let b = [];
    let c = {};

    let d = "";
    let e = 0;
    let f = null;
    let g;

    !!a; // true
    !!b; // true
    !!c; // true

    !!d; // false
    !!e; // false
    !!f; // false
    !!g; // false

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

如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用ToPrimitive抽象操作,该抽象操作再调用[[DefaultValue]],以数字作为上下文。

    let a = "42";
    let b = "0";

    let c = 42;
    let d = 0;

    // 操作数都是字符串,所以进行的是字符串拼接
    a + b; // "420"
    // c,d 是 number 类型,本身就是原始值
    c + d; // 42

JavaScript 中的 [[DefaultValue]] 规范是 JavaScript 引擎内部的一个抽象操作,用于确定如何将一个值转换为原始值(primitive value)。

[[DefaultValue]] 规范包括两个参数,一个是 hint,另一个是 preferredType

  • hint 表示希望将对象转换为什么类型的原始值,可以是 string 或 number。
  • preferredType 表示首选的类型,如果 hint 无法确定时会使用。

根据这两个参数的不同组合,[[DefaultValue]] 规范会执行不同的转换操作。以下是一些示例:

  1. 如果 hintpreferredType 都未指定,[[DefaultValue]] 将首先尝试转换为字符串(preferredType 为 “string”),如果无法成功,然后再尝试转换为数字。
  2. 如果 hint 为 string,则 [[DefaultValue]] 会尝试调用对象的 toString() 方法来获取字符串表示。如果没有 toString() 方法,它将尝试调用 valueOf() 方法。
  3. 如果 hint 为 number,则 [[DefaultValue]] 会尝试调用对象的 valueOf() 方法来获取数值表示。如果没有 valueOf() 方法,它将尝试调用 toString() 方法。
  4. 如果 hintpreferredType 均指定,并且指定的方法返回的结果不是合法的原始值,则引发 TypeError。

preferredType 参数在实际应用中使用得相对较少,通常默认情况下不需要显式指定它,JavaScript 会根据 hint 来执行适当的转换。

    let a = [1, 2];
    let b = [3, 4];

    // a,b 操作数都是数组,在JavaScript中数组也是对象的一种
    // 进行 ToPrimitive 抽象操作
    a + b; // "1,23,4"

|| 和 &&

逻辑运算符||(或)&&(与)或者称它们为“选择器运算符”(selector operators)或者“操作数选择器运算符”(operand selector operators)

在JavaScript中它们返回的并不是布尔值。它们的返回值是两个操作数中的一个(且仅一个)。即选择两个操作数中的一个,然后返回它的值。

    let a = 42;
    let b = "abc";
    let c = null;

    a || b; // 42
    a && b; // "abc"

    c || b; // "abc"
    c && b; // null

||&&首先会对第一个操作数(a和c)执行条件判断,如果其不是布尔值就先进行ToBoolean强制类型转换,然后再执行条件判断。


逻辑或:

对于||来说,如果左操作数判断结果为true返回第一个操作数(a和c)的值,如果为false返回第二个操作数(b)的值


逻辑与:

&&则相反,如果左操作数判断结果为true就返回第二个操作数(b)的值,如果为false返回第一个操作数(a和c)的值

|| 与 && 的短路机制

&&||来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数。即执行最短路径

a && b为例,如果a是一个假值,足以决定&&的结果,就没有必要再判断b的值。同样对于a || b,如果a是一个真值,也足以决定||的结果,也就没有必要再判断b的值。

运算符优先级

&&运算符的优先级高于||,而||的优先级又高于? :

宽松相等和严格相等

宽松相等(loose equals)==严格相等(strict equals)===都用来判断两个值是否“相等”。==允许在相等比较中进行强制类型转换,而===不允许。就从结论上来说“==检查值是否相等===检查值和类型是否相等”。

对象(包括函数和数组)的宽松相等 ==。两个对象指向同一个值时即视为相等,不发生强制类型转换

字符串和数字之间的相等比较
    let a = 42;
    let b = "42";

    a === b; // false
    a == b;  // true

a == b是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型转换

(1) 如果Type(x)是数字,Type(y)是字符串,则返回 x == ToNumber(y)的结果。

(2) 如果Type(x)是字符串,Type(y)是数字,则返回ToNumber(x) == y的结果。

进行==比较时,如果一个是数字,一个是字符串,那么字符串被强制转换为数字,使用toNumber规则转换。

其他类型和布尔类型之间的相等比较
    let x = true;
    let y = "42";

    x == y; // false

Type(x)是布尔值,所以ToNumber(x)将true强制类型转换为1,变成1 == “42”,二者的类型仍然不同,"42"根据规则被强制类型转换为42,最后变成1 == 42,结果为false。

(1) 如果Type(x)是布尔类型,则返回ToNumber(x) == y的结果;

(2) 如果Type(y)是布尔类型,则返回x == ToNumber(y)的结果。

null和undefined之间的相等比较
    let a = null;
    let b; // undefined

    a == b;     // true
    a == null;  // true
    b == null;  // true

    a == false; // false
    b == false; // false
    a == "";    // false
    b == "";    // false
    a == 0;     // false
    b == 0;     // false

(1) 如果x为null, y为undefined,则结果为true。

(2) 如果x为undefined, y为null,则结果为true。

在 == 中,null与undefined互相相等,也就是说可以相互进行隐式强制类型转换,且等于自身。但不与其他任何值相等。比如null不等于false

对象和非对象之间的相等比较
    let a = 42;
    let b = [42];

    a == b; // true

(1) 如果Type(x)是字符串或数字,Type(y)是对象,则返回x == ToPrimitive(y)的结果;

(2) 如果Type(x)是对象,Type(y)是字符串或数字,则返回ToPrimitive(x) == y的结果。

自动分号插入(ASI)

有时JavaScript会自动为代码行补上缺失的分号,即自动分号插入(Automatic Semicolon Insertion,ASI

ASI只在换行符处起作用,而不会在代码行的中间插入分号

如果JavaScript解析器发现代码行可能因为缺失分号而导致错误,那么它就会自动补上分号。并且,只有在代码行末尾与换行符之间除了空格和注释之外没有别的内容时,它才会这样做。

暂时性死区(TDZ)

TDZ指的是由于代码中的变量还没有初始化而不能被引用的情况。这个错误发生在变量被声明之后,但在赋值之前的阶段。

     {
       // 未捕获的ReferenceError:初始化前无法访问“a”
       a = 2; // Uncaught ReferenceError: Cannot access 'a' before initialization
       let a;
     }

     {
       typeof a;   // undefined
       typeof b;   // ReferenceError! (TDZ)
       let b;
     }

注意:

使用 var 声明的变量不会进入暂时性死区,当使用 var 声明变量时,变量会被提升(hoisting),这意味着变量的声明会在代码执行之前被移到当前作用域的顶部。这使得你可以在声明之前引用 var 变量而不会引发错误。然而,如果你在声明之前访问 var 变量,它会返回 undefined

     console.log(x); // 不会抛出错误,但会打印 "undefined"
     // var 声明的变量会存在变量提升
     var x = 42; // 声明并初始化变量
     console.log(x); // 打印 "42"

try…finally

finally中的代码总是会在try之后执行,如果有catch的话则在catch之后执行。也可以将finally中的代码看作一个回调函数,即无论出现什么情况最后一定会被调用。

在try中出现return的情况

     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(…)显示返回值。

在try中出抛出异常的情况

    function foo() {
      try {
        throw 42;
      }
      finally {
        console.log("Hello");
      }

      console.log("never runs");
    }

    console.log(foo());
    // Hello
    // Uncaught Exception: 42

如果finally中抛出异常(无论是有意还是无意),函数就会在此处终止。如果此前try中已经有return设置了返回值,则该值会被丢弃

     function foo() {
       try {
         // 被丢弃!!
         return 42;
       }
       finally {
         throw "Oops! ";
       }
     ​
       console.log("never runs");
     }
     ​
     console.log(foo());
     // Uncaught Exception: Oops!

在finally中出现return的情况

finally中的return会覆盖try和catch中return的返回值

     function baz() {
       try {
         return 42;
       }
       finally {
         // 覆盖前面的return 42
         // 只要出现 return 就覆盖 try 中的 return
         return "Hello";
       }
     }
     baz(); // "Hello"

switch

     switch (a) {
       case 2:
         // 执行一些代码
         break;
       case 42:
         // 执行另外一些代码
         break;
       default:
       // 执行缺省代码
     }

acase表达式的匹配算法与 ===相同(严格相等)。

<script>标签

通常可以在网页中使用<script src=..></script>来加载这些文件,或者使用<script> .. </script>来包含内联代码(inline-code),而且它们共享global对象(在浏览器中则是window),也就是说这些文件中的代码在共享的命名空间中运行,并相互交互

     <script>foo(); </script><script>
     function foo() { ..}
     </script>

在第一个script中定义了函数foo(),后面的script代码就可以访问并调用foo(),就像foo()在其内部被声明过一样。


但是全局变量作用域的提升机制(hoisting)在这些边界中不适用,因此无论是<script> .. </script>还是<script src=..></script>,简单的来说就是只会在自己的script标签中进行变量提升,自己的script标签便是边界。

     <script>
      // 函数会变量提升
     foo();
     function foo() { .. }
     </script>

但是即便是提升,也不会越过自己的边界:

     <script>foo(); </script><script>
     function foo() { .. }
     </script>

若更改script的顺序则可以运行,如:

     <script>
     function foo() { .. }
     </script><script>foo(); </script>

如果script中的代码(无论是内联代码还是外部代码)发生错误它会像独立的JavaScript程序那样停止,但是后续的script中的代码(仍然共享global)依然会接着运行,不会受影响


参考资料: 《你不知道的JavaScript中卷》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

读书的小蜗牛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值