JS篇之数据类型那些事儿

324 阅读8分钟

人的不幸来源于他不肯安分守己地待在自己应该待的房里里 --《香水》

一语中的

  1. JS = ECMAScript + DOM + BOM
  2. DOM 并非只能通过 JS 访问
  3. JS是动态弱类型语言
  4. 每个变量只不过是一个用于保存任意值的命名占位符
  5. 实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有
  6. 基本类型是没有任何属性和方法
  7. 对象其实就是一组数据和功能的集合

文章概要

  • JS组成
  • JS数据类型(7+1)
  • 类型转换(装箱/拆箱)

JS组成

其实这是一个很容易忽略的问题。俗话说,最熟悉的陌生人说的就是这种情况。

浏览器环境下JS = ECMAScript + DOM + BOM

我们来简单介绍下。(后期会单独针对BOM/DOM进行分析)

ECMAScript

JS的核心部分,即 ECMA-262 定义的语言,并不局限于 Web 浏览器。

Web 浏览器只是 ECMAScript 实现可能存在的一种*宿主环境(host environment)。而宿主环境提供ECMAScript 的基准实现和与环境自身交互必需的扩展。(比如 DOM 使用 ECMAScript 核心类型和语法,提供特定于环境的额外功能)。

像我们比较常见的Web 浏览器Node.js和即将被淘汰的 Adobe Flash都是ECMA的宿主环境。

ECMAScript 只是对实现ECMA-262规范的一门语言的称呼, JS 实现了ECMAScript,Adobe ActionScript 也实现ECMAScript。

文档对象模型(DOM)

DOM是一个应用编程接口(API),通过创建表示文档的树,以一种独立于平台和语言的方式访问和修改一个页面的内容和结构。

在HTML文档中,Web开发者可以使用JS来CRUD DOM 结构,其主要的目的是动态改变HTML文档的结构。

DOM 将整个页面抽象为一组分层节点

DOM 并非只能通过 JS 访问, 像可伸缩矢量图(SVG)、数学标记语言(MathML)和同步多媒体集成语言(SMIL)都增加了该语言独有的 DOM 方法和接口。

浏览器对象模型(BOM)

用于支持访问和操作浏览器的窗口。

针对浏览器窗口和子窗口(frame)提供了

  • 弹出新浏览器窗口的能力
  • 移动、缩放和关闭浏览器窗口的能力
  • navigator 对象,提供关于浏览器的详尽信息
  • location 对象,提供浏览器加载页面的详尽信息
  • screen 对象,提供关于用户屏幕分辨率的详尽信息
  • performance 对象,提供浏览器内存占用、导航行为和时间统计的详尽信息
  • 对 cookie 的支持
  • 其他自定义对象,如 XMLHttpRequest 和 IE 的 ActiveXObject

JS数据类型

每种编程语言都具有内建的数据类型,而根据使用数据的方式从两个不同的维度将语言进行分类。

  1. 动态/静态
  • 动态类型:运行过程中需要检查数据类型
  • 静态类型:使用之前就需要确认其变量数据类型
  1. 强/弱
  • 强类型:不支持隐式类型转换
  • 弱类型:支持隐式类型转换

隐式类型转换 :在赋值过程中,编译器会把 int 型的变量转换为 bool 型的变量

通过上述的介绍和平时大家的使用JS的数据类型发现。

JS是动态弱类型语言。

由于JS的语言特性,我们可以进而得出另外一个结论:每个变量只不过是一个用于保存任意值的命名占位符。

而谈到JS数据类型,就绕不开针对数据的分类。你没猜错,还是一样的配方,大家熟悉的味道。

ECMAScript 有8 种数据类型

  1. Undefined
  2. Null
  3. Boolean
  4. String
  5. Number
  6. Symbol (ES6新增)
  7. BigInt (ES2020新增)
  8. Object (基本引用类型、)

根据数据存储位置的不同,我们将JS数据类型分为两大类:

  1. 基本数据类型(primary) 存放在栈内存中,类型1-7
  2. 复杂数据类型/引用类型 存放在堆内存中, 类型8

针对老生常谈的问题,我们来搞点不一样的。

JS 判断数据类型方式(4种)

该问题在一些面试题中,出现的频率还挺高。(敲黑板,考试要考!)

1. typeof

typeof 操作符可以确定值的原始类型,也就是说,该操作只能区分基本数据类型,而对于复杂数据类型就鞭长莫及了。

let un, nu =null, bo = true, st = '789', 
    num = 789,sy = Symbol('789'),bi = 789n;
typeof un; // "undefined"
typeof nu; // "object" 这是一个特例,或者说null就是一个特例
typeof bo; //"boolean"
typeof st; // "string"
typeof num;// "number"
typeof sy; // "symbol"
typeof bi; // "bigint"

特例分析: null值表示一个空对象指针。所以针对typeof null 返回了一个"object"。

2. Object.prototype.toString.call(xx)

若参数(xx)不为 null 或 undefined,则将参数转为对象,再作判断。转为对象后,取得该对象的 [Symbol.toStringTag] 属性值(可能会遍历原型链)作为 tag,然后返回 "[object " + tag + "]" 形式的字符串。

针对基本数据类型,通过装箱过程转为对象类型。

Object.prototype.toString.call(null) //[ojbect Null]
Object.prototype.toString.call(undefined) //[object Undefined]
Object.prototype.toString.call(true) // [object Boolean]
Object.prototype.toString.call(()=>{}) // [object Function]

通过Object.prototype.toString可以将数据类型很容易的分开。但是,每次进行判断的时候,多了一堆额外的信息。所以,我们可以对该方法进行改进。

function getDataType(type){
  return Object.prototype.toString.call(type)
  .split(' ')[1]
  .slice(0,-1)
  .toLocaleLowerCase();
}

getDataType(null) //null
getDataType(undefined) // undefined
getDataType(true) // boolean
getDataType(()=>{}) // function

3. instanceof

在一些资料中讲到,instanceof 是用来判断 a 是否为 B 的实例,表达式为:a instanceof B,如果 a 是 B 的实例,则返回 true,否则返回 false

其实这句话是不严谨的。准确的描述应该是:a instanceof B 用于判断实例a原型链中出现过相应的构造函数B,则 instanceof 返回 true 。 instanceof 判断的是 a和B是否有血缘关系,而不是仅仅根据是否是父子关系。

let ar = [];
ar instanceof Array // true
ar instanceof Object // true  如果按照实例的关系的话,这应该返回false

多说一句,在ES6中instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系。

这个属性定义在 Function 的原型上,因此默认在所有函数和类上都可以调用。

function Car() {} 
let c = new Car(); 
console.log(Car[Symbol.hasInstance](c)); // true 

我们可以通过重新定义该方法,来改变instanceof的值。

class Parent{} 
class Child extends Parent { 
  static [Symbol.hasInstance]() { 
   return false; 
  }  
} 
let c = new Child(); 
console.log(Parent[Symbol.hasInstance](c)); // true 
console.log(c instanceof Parent); // true 
// 
console.log(Child[Symbol.hasInstance](c)); // false 
console.log(c instanceof Child); // false

(这里再埋一个伏笔,后期会有针对原型的文章)

4. constructor

只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。

每次调用构造函数创建一个新实例,实例的内部[[Prototype]]指针就会被赋值为构造函数的原型对象。

实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有

所以,可以通过实例和构造函数原型的关系,来判断是否实例类型。

''.constructor === String; // true;
true.constructor === Boolean; // true;
new Number(1).constructor === Number // number 类型存在包装对象

null/undefined是一个假值,没有对应包装对象(无法进行装箱操作),也不是任何构造函数的实例。所以,不存在原型,即,无法使用constructor判断类型。


类型转换(装箱/拆箱)

基本类型是没有任何属性和方法

此时,有人就会有一个疑问,当定义了let str = '789';,此时可以通过str进行属性和方法调用。这不是和上面的那个相悖嘛。

其实,针对基本类型的属性和方法的调用,都是在基本类型的包装对象上进行操作。

装箱转换

每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

let str = '789';
str.length; //3 属性调用
str.slice(1); // "89"  方法调用

=======等价于
let strObj = new String(789);
strObj.length; //3
strObj.slice(1); //"89"

拆箱转换

在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError

let o = {
      valueOf : () => {console.log("valueOf"); return {}},
      toString : () => {console.log("toString"); return {}}
     }
 
  o * 2
 // valueOf
 // toString
 // TypeError

对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。

let o = {
        valueOf : () => {console.log("valueOf"); return {}},
        toString : () => {console.log("toString"); return {}}
    }
 
 o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
 
 console.log(o + "")
 // toPrimitive
 // hello