美文网首页细品 JavaScript
细读 JS | 数据类型详解

细读 JS | 数据类型详解

作者: 越前君 | 来源:发表于2021-05-04 16:48 被阅读0次

    今天又又又...又整理了一下,那些 JavaScript 里不清不楚的知识点。

    一、数据类型的分类

    截止发文日期,ECMAScript 标准的数据类型仅有 8 种(ECMAScript Language Types)。可以分为两类:

    • 原始类型(Primitives),我们也称作基本数据类型
      • Undefined
      • Null(一种特殊的原始类型,typeof(instance) === 'object'
      • Boolean
      • String
      • Symbol(typeof(instance) === 'symbol'
      • Number
      • BigInt(typeof(instance) === 'bigint'
    • 引用类型(Objects)
      • Object(包括从 Object 派生出来的结构类型,如 Object、Array、Map、Set、Date 等)

    关于使用 typeof 判断以上数据类型的话题,老生常谈了。例如,为什么 typeof null === 'object'typeof(() => {}) === 'function' 呢?这里不展开赘述了,请移步:JavaScript 的迷惑行为大赏

    原始类型的比较的是值,只有两者的值相等,那么它们被认为是相等的,否则不相等。而引用类型比较的是地址,当两者的标识符同时指向内存的同一个地址,则被认为是相等的,否则不相等。

    console.log({} == {}) // false
    console.log([] == []) // false
    

    二、原始类型与原始值

    所有基本类型的值(即原始值,Primitive Values)都是不可改变(immutable)的,而且不含任何属性和方法的。

    到这里可能会有小伙伴打问号了???

    Q1:原始类型与原始值有什么区别?

    原始类型的值称为原始值。例如原始类型 Boolean 有两个(原始)值 truefalse。同样的原始类型 Undefined(Null),只有一个原始值 undefinednull)。其他的就有很多个了...

    Q2:原始值不可改变?这样不是改变了吗?

    var foo = true
    foo = false
    console.log(foo) // false
    

    其实不然,以上示例是原始类型和一个赋值为原始类型的变量的区别。变量会被赋予一个新值,而原值不能像数组、对象以及函数那样被改变。

    基本类型值可以被替换,但不能被改变。

    // 使用字符串方法不会改变一个字符串
    var foo = 'foo'
    foo.toUpperCase()
    console.log(foo) // "foo"
    
    // 赋值行为可以给基本类型一个新值,而不是改变它
    foo = foo.toUpperCase() // "FOO"
    

    再有示例:

    var num = 1
    
    function add(num) {
      num += 1
      console.log(num)
    }
    
    add(num) // 2
    console.log(num) // 1
    
    // ************************** 华丽的分割线 **************************
    
    // 如果没有看上面的一些概念,单纯地看上面的例子,我相信百分百都能得到正确答案。
    // 但看完上面一些的概念之后,再结合例子,不知道会不会有人对 “原始类型的值不可改变” 这句话产生怀疑?
    // 如果有怀疑就继续往下看 👇👇👇,否则可直接跳到 Q3 了。
    
    // ************************** 华丽的分割线 **************************
    
    // JS 运行的三个步骤:词法分析、预编译、解析执行。
    // 其中预编译,不仅仅发生在 script 代码块执行之前,还发生在函数执行之前。
    // 
    // 函数预编译的过程大致是这样的:
    // 1. 首先查找形参和变量声明(此时并赋予值 undefined)
    // 2. 接着将实参赋值给形参
    // 3. 接着查找函数体内的函数声明(赋予函数本身)。
    //
    // 函数 add 在实参赋值给形参的过程,会将传递进来的参数(基本类型的值)复制一份,
    // 创建一个本地副本,该副本只存在于该函数的作用域中。(原本的值与副本是完全独立,互不干扰的)
    

    Q3:原始值没有任何属性和方法?那这个是怎么回事?

    var foo = 'foo'
    console.log(foo.length) // 3
    console.log(foo.toUpperCase()) // "FOO"
    
    // 试图改变 length 属性
    foo.length = 4
    console.log(foo.length) // 3
    

    其实这是 JavaScript 包装类的内容了。

    在 JavaScript 中除了 nullundefined 之外,所有的基本类型都有其对应的包装对象(Wrapper Object)。因此,访问 nullundefined 的任何属性和方法都会抛出错误。

    • String 为字符串基本类型。
    • Number 为数值基本类型。
    • BigInt 为大整数基本类型。
    • Boolean 为布尔基本类型。
    • Symbol 为字面量基本类型。

    这些包装对象的 valueOf方法返回其对应的原始值。

    再次明确一点,原始值是没有任何属性和方法的。

    不是说好的,原始值不含任何的属性和方法吗?那 foo.lengthfoo.toUpperCase() 是咋回事啊???

    其实它内部是这样实现的:当字符串字面量调用一个字符串对象才有的方法或属性时,JavaScript 会自动将基本字符串转化为字符串对象并且调用相应的方法或属性。(Boolean 和 Number 也同样如此)。

    我们尝试在控制台上打印一下 new String('foo'),可以看到该实例对象有一个 length 属性,其值为 3,实例对象本身没有 toUpperCase() 方法,所以接着往原型上查找,果然找到了。(由于原型上方法太多,截图里没有展开,否则影响文章篇幅)

    因此

    var foo = 'foo'
    console.log(foo.length) // 3
    console.log(foo.toUpperCase()) // "FOO"
    
    // 相当于
    var foo = 'foo'
    console.log(new String(foo).length) // 3
    console.log(new String(foo).toUpperCase()) // "FOO"
    

    可下面为什么 length 还是 3 呢?

    foo.length = 4
    console.log(foo.length) // 3
    
    // 怎样理解呢?
    //
    //
    // 执行第一行代码
    // foo.length = 4 可以拆分成两部分去理解:
    var temp = new String(foo) // 在内存中创建了一个对象,只是没有一个标识符(变量)指向它而已(为了便于理解,我这里假装有一个 temp 变量指向它)
    temp.length = 4 // 修改包装对象的 length 属性,其实是修改成功的
    // 由于该对象并没有被引用,所以在执行下一句代码之前就被回收销毁了
    //
    //
    // 2. 执行第二行代码
    // console.log(foo.length) 相当于
    console.log(new String(foo).length) // foo 还是 "foo",自然结果就是 3 了。
    

    三、对象

    在 JavaScript 中,除了以上的原始值,其余都属于对象。

    与原始类型不同的是,对象是可变(mutable)的。

    1. 对象的分类

    我们可以将对象划分为普通对象(ordinary object)和函数对象(function object)。

    那怎样区分呢?我们先定义一些 Function 实例和 Object 实例:

    // Function 实例
    function fn1() {}
    var fn2 = function() {}
    var fn3 = new Function('console.log("Hi, everyone")') // 一般不使用 Function 构造器去生成 Function 对象,相比函数声明或者函数表达式,它表现更为低效。
    
    // Object 实例
    var obj1 = {}
    var obj2 = new Object()
    var obj3 = new fn1()
    

    我们来打印一下结果:

    typeof Object     // "function"
    typeof Function   // "function"
    
    typeof fn1        // "function"
    typeof fn2        // "function"
    typeof fn3        // "function"
    
    typeof obj1       // "object"
    typeof obj2       // "object"
    typeof obj3       // "object"
    

    ObjectFunction 本身就是 JavaScript 中自带的函数对象。其中 obj1obj2obj3 为普通对象(均为 Object 的实例),而 fn1fn2fn3 为函数对象(均是 Function 的实例)。

    记住以下这句话:

    所有 Function 的实例都是函数对象,而其他的都是普通对象

    2. 对象的原型

    接着,引入两个很容易让人抓狂、混淆的两兄弟 prototype (原型对象)和 __proto__(原型)。这俩兄弟的主要是为了构造原型链而存在的。

    对象类型 prototype __proto__
    普通对象
    函数对象

    因此有以下结论:

    所有对象都有 __proto__ 属性,而只有函数对象才具有 prototype 属性。

    再上几个菜,请慢慢品尝:

    // 每个对象都有一个 constructor 属性,该属性指向实例对象的构造函数
    Object.prototype.constructor === Object // true
    Function.prototype.constructor === Function // true
    
    
    // (全局对象)Object 是 (构造器)Function 的实例
    // (全局对象)Function 也是 (构造器)Function 的实例
    Object.__proto__ === Function.prototype // true
    Function.__proto__ === Function.prototype // true
    
    
    // (构造器)Function 也是(构造器)Object 的实例
    Function.prototype.__proto__ === Object.prototype // true
    
    
    // 从原型上查找属性,不可能无终止地查找下去,那原型的尽头在哪呢?
    // 站在原型顶端的男人,是它。
    // 假设我们访问一个对象的属性或者方法,如若前面的原型上均无法查找到,最终会止步于此,并返回 undefined。
    Object.prototype.__proto__ // null
    

    在 JavaScript 中访问一个对象属性,它在原型上是怎样查找的呢?

    function Person() {} // 构造函数
    var person = new Person() // 实例化对象
    console.log(person.name);  // undefined
    
    // 过程如下:
    person // 是对象,可以继续
    person['name'] // 不存在属性 name,继续查找
    person.__proto__ // 是对象,可以继续
    person.__proto__['name'] // 不存在属性 name,继续查找
    person.__proto__.__proto__ // 是对象,可以继续
    person.__proto__.__proto__['name'] // 不存在属性 name,继续查找
    person.__proto__.__proto__.__proto__ // 不是对象,是 null 值。停止查找,返回 undefined
    

    需要注意的是,Object.prototype.__proto__ 从未被包括在 ECMAScript 语言规范中标准化,但它被大多数浏览器厂商所支持。该特性已从 Web 标准中删除,详情可看 Object.prototype.__proto__

    在标准中,几乎(例外是 Object.create(null) ,下面有说明)每个实例对象内部都有一个 [[Prototype]] 属性,该属性指向对象的原型,而且该属性值只会是对象或者 null

    在非标准下,可以通过 Object.prototype.__proto__ 访问(或设置)实例对象内部的 [[Prototype]],这种方式其实是不被推荐使用的。现在更被推荐使用的方式是 Objec.getPrototypeOf()/Object.setPrototypeOf()

    请注意,以上(包括下文)所指对象均不是通过 Object.create(null) 实例化的(除特意说明外)。Object.create(null) 实例化的对象比较特殊,它内部没有 [[Prototype]] 属性,也没有任何其他内部属性。(Object.create()

    var obj = Object.create(null)
    
    var obj1 = Object.create(null)
    var obj2 = {}
    
    obj.__proto__ === undefined // true
    obj.getPrototypeOf() // 抛出错误 TypeError: obj.getPrototypeOf is not a function
    

    我们可以在控制台打印一下,看下两者的区别。

    JavaScript 常被描述为一种基于原型的语言 —— 每个对象拥有一个原型([[Prototype]]),对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链(prototype chain)。

    3. 继承

    关于继承内容,可看另外一篇文章:深入 JavaScript 继承原理

    4. 对象的内部属性(Internal properties)

    在规范中,对象的内部方法和内部插槽使用双方括号 [[]] 中包含的名称标识,且首字母为大写。例如 [[Prototype]][[Class]][[Extensible]][[Call]][[Scopes]][[FunctionLocation]] 等等。

    下面挑几个来讲一下:

    4.1 [[Class]]

    [[Class]] 是对象的一个内部属性,其值为以下字符串之一:

    • 常见的有:FunctionObjectArrayBooleanNumberStringSymbolRegExpJSONDateMathErrorArguments 等。
    • 比较少用的有:BigIntSetWeakSetMapWeakMapReflectPromiseGeneratorFunctionAsyncFunctionWindowIntlWebAssembly,以及派生于 HTMLElement 的(如 HTMLScriptElement )等等。
    • 几乎所有标准内置对象,都有特定的类型。实在太多了...

    我们都知道 typeof 无法判断对象的具体类型,无论是 typeof {}typeof []、还是 typeof Math 都返回 "object"。但有了 [[Class]] 属性之后,我们就可以利用它来判断对象的类型了。访问 [[Class]] 的唯一方法是通过默认的 toString() 方法(该方法是通用的):

    Object.prototye.toString()

    • 如果参数 undefined,则返回 [object Undefined] 字符串;
    • 如果参数 null,则返回 [object Null] 字符串;
    • 如果参数是一个对象,则返回 "[object " + obj.[[Class]] + "]" 字符串,例如 [object Array]
    • 如果参数是一个原始值,则会先将其转换为相应的对象,然后按照以上的规则输出。

    以下封装了获取对象类型的方法:

    function getClass(x) {
      const { toString } = Object.prototype
      const str = toString.call(x)
      return /^\[object (.*)\]$/.exec(str)[1]
    }
    
    getClass(null) // "Null"
    getClass(undefined) // "Undefined"
    getClass({}) // "Object"
    getClass([]) // "Array"
    getClass(JSON) // "JSON"
    getClass(() => {}) // "Function"
    ;(function() { return getClass(arguments) })() // "Arguments"
    

    4.2 [[Construct]]

    一个对象里,如若没有 [[construct]] 属性,是无法使用 new 关键字进行构造的。

    四、类型转换

    在 JavaScript 中,我们会经常使用相等运算符(==)去比较两个操作数是否相等。当两个操作数一个是引用类型,另一个是原始类型的时候,前者会先转换为原始类型,再比较。

    那么,引用类型是如何转换为原始类型的呢?

    关于 JavaScript 类型转换的内容,已经单独写了一篇文章详细地介绍了,请看 👉 Type Conversion 详解

    未完待续...

    参考

    相关文章

      网友评论

        本文标题:细读 JS | 数据类型详解

        本文链接:https://www.haomeiwen.com/subject/fagxdltx.html