携手创作,共同成长!这是我参与「日新计划 8 月更文挑战」的第 3 天,点击查看活动详情

这是一个基础结构问题,一个完整的 JavaScript 包括:ECMAScript 、文档对象模型(DOM)、浏览器对象模型(BOM),本章讲解 ECMAScript

ECMAScript 规定这门语言由以下组成:

  1. 语法
  2. 变量和数据类型
  3. 关键字和保留字
  4. 操作符
  5. 语句
  6. 对象

也就是说,这六大模块组成了这门语言

这六大支柱中,像「语法」、「变量」和「数据类型」、「关键字」和「保留字」、「操作符」、「语句」都是极好理解的,只「对象」有点复杂,下节会对其进行说明,这里我们来看看「数据类型」

数据类型

JavaScript 的数据类型分为「基本类型」和 「引用类型」

基本类型在不同的书(教程中)叫法不同,它同样被叫做基本类型/值类型/原始值/原始类型。当然引用类型也有不一样的叫法,如对象类型/复杂类型

基本类型包含 undefined 、null、string、number、boolean、symbol(ES6 新增)、bigint(ES10新增)

引用类型则是object(一组属性的集合)

特此说明:

在《JavaScript 高级程序设计第四版》中曾把数据类型归纳为驼峰形式的Undefined、Null、Boolean、Number、String 、 Symbol、BigInt、Object,而在网站现代 JavaScript 教程、MDN中则以小写的形式展示展示数据类型,笔者这里以网站为更大依据

基本类型和引用类型的区别

基本类型储存在栈内存中

引用类型储存在堆内存中

基本类型花销的内存小,所以拷贝值

引用类型花销的内存大,所以拷贝引用地址

深入理解JavaScript-JavaScript 由什么组成

// 案例1
var string1 = 'foo';
var string2 = string1;
string1 = 'bar';
console.log(string1, string2); // 输出 bar, foo
// 案例2
var object1 = {
    name: 'johan',
    age: 23,
};
var object2 = object1;
object1.name = 'elaine';
object2.age = 22;
console.log(object1, object2);
// 输出 { name: 'elaine', age: 22 } { name: 'elaine', age: 22 }

因为基本类型是存放在栈内存中,string1 赋值给 string2 后,相当于开了一个独立空间给 string2,string1 和 string2 是完全独立的两个个体,再给 string1 赋值什么,都与 string2 无关

而引用类型之所以叫引用类型(或叫复杂类型/复杂值),因为对象的地址是引用的,Object 可以一直嵌套,Function 也可以嵌套很多很多层(回调函数),Array 可以变花样的做出三维数组之类,总之,对象值可以无限大。值越大,就越占内存,如果一个很大的值随意复制,那对使用者来说就是灾难(不知不觉中就占了很大的内存)。所以它不可能随意复制,而是用指针的方式来将对象指向同一个地址(这里可以衍生到深拷贝)

当你创建一个 object1,就在堆内存中开辟一个内存。如果你赋值给 object2,其实就是把 object1 指向堆内存的地址复制给 object2,它们指向的是同一个地址。再在 object1 或者 object2 做修改的话,相当于在同一内存上做修改,无论 object1 修改还是 object2 修改都会改变

var obj1 = {};
var obj2 = obj1;
obj1.name = 'elaine';
obj2.age = 23;
console.log(obj1); // { name: 'elaine', age: 22};
console.log(obj2); // { name: 'elaine', age: 22};

衍生思考:虽然说 JavaScript 的动态性很是方便,但是如果一些新手修改了对象类型的值而不告知,那么就成了灾难。因为动态性+全局作用域,所有变量命名就成了问题,所以一般的库都是用IIFE、闭包等方法破局,再后面就有了模块化(ES module)的概念,其本质是为了解决变量命名、语言动态性的问题

光知道 JavaScript 的数据类型不够,如何更准确的知道每个数据类型

判断数据类型的方式有四种

  • 使用 typeof
  • 使用 instanceof
  • 使用 constructor
  • 使用 Object.prototype.toString.call()

类型判断

typeof 操作符

作用:返回正在使用的值的基本类型

// 基本类型/值类型/原始值/原始类型	
var null1 = null;
var undefined1 = undefined;
var string1 = 'foo';
var number1 = Number('10');
var boolean1 = Boolean('true');
var symbol1 = Symbol('foo');
console.log(typeof null1); // object, 需要注意
console.log(typeof undefined1); // undefined
console.log(typeof string1); // string
console.log(typeof number1); // number
console.log(typeof boolean1); // boolean
console.log(typeof symbol1); // symbol
// 引用类型/复杂值
var myString = new String('male');
var myNumber = new Number(23);
var myBoolean = new Boolean(false);
var myObject = new Object();
var myArray = new Array('foo', 'bar');
var myFunction = new Function('x', 'y', 'return x * y');
var myDate = new Date();
var myRegExp = new RegExp('\bt[a-z]+\b');
var myError = new Error('error');
console.log(typeof myString); // 输出 object
console.log(typeof myNumber); // 输出 object
console.log(typeof myBoolean); // 输出 object
console.log(typeof myObject); // 输出 object
console.log(typeof myArray); // 输出 object
console.log(typeof myFunction); // 输出 function 需要注意
console.log(typeof myDate); // 输出 object
console.log(typeof myRegExp); // 输出 object
console.log(typeof myError); // 输出 object

提示:typeof xxx 和 typeof(xxx) 一个意思

通过 typeof 操作符能判断出使用的值的类型。需要注意判断引用类型时的问题

  1. null 类型会返回 object
  2. new Function,返回的是 function

随便一说,这也是我们看很多源码或者自己写代码时常用 typeof 来判断 function 类型

if (typeof XXX === 'function') {
    // 如果XXX是function的话做什么操作
}

PS:《JavaScript 启示录》里第一章第八节里说 RegExp() 的类型返回的是 function,但是笔者检验后发现并不是,笔者猜测因为老版本的浏览器对 RegExp 的判断为 funtion,而笔者用 Chrome 浏览器表示 RegExp 的类型为 object

instanceof 运算符

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

function People(name, age) {
    this.name = name;
    this.age = age;
}
const elaine = new People('elaine', 23);
console.log(elaine instanceof People);
console.log(elaine instanceof Object);

instanceof 运算符能让我们找到它的爸爸是谁(谁制造了它,从生理上讲应该是妈妈是谁),以及它的祖宗十八代,这也是面试中常遇到的——instanceof 的原理是什么

constructor 构造方法

constructor 是一种用于创建和初始化 class 创建的对象的特殊方法。请注意,它是函数(方法)

就好比:

function sayHello() {
    console.log('hello');
}

不要因为单词陌生误以为它是属性

语法:

constructor([arguments]) { ... }

这里多说一句,React 类组件是这样写的:

class HelloWorld extends React.Component {
    constructor(props) {
        super(props);
    }
}

这里的意思很明白,HelloWorld 组件继承了 React.Component 组件,constructor(props) 意味着调用了父类的构造函数,并将 props 传递给 HelloWorld,使用 super 是因为在派生类中,必须调用 super 才能使用 this,当然,这部分就关系到 class 的知识,在此不表

关于构造函数(constructor)需要知道的是它是 Object 原型上的方法,即 Object.prototype.constructor,所有对象都会有 constructor 属性,它指向该对象的构造函数,关于继承后续文章会讲解

说完题外话,我们继续来看 constructor 能否检验数据类型吗

// 借 typeof 中的例子,直接打印看效果
// 基本类型
console.log(null1.constructor); // Cannot read properties of null (reading 'constructor')
console.log(undefined1.constructor); // Cannot read properties of null (reading 'constructor')
console.log(string1.constructor); // String() { [native code] }
console.log(number1.constructor); // Number() { [native code] }
console.log(boolean1.constructor); // Boolean() { [native code] }
console.log(symbol1.constructor); // Symbol() { [native code] }
// 引用类型
console.log(myString.constructor); // String() { [native code] }
console.log(myNumber.constructor); // Number() { [native code] }
console.log(myBoolean.constructor); // Boolean() { [native code] }
console.log(myObject.constructor); // Object() { [native code] }
console.log(myArray.constructor); // Array() { [native code] }
console.log(myFunction.constructor); // Function() { [native code] }
console.log(myDate.constructor); // Date() { [native code] }
console.log(myRegExp.constructor); // RegExp() { [native code] }
console.log(myError.constructor); // Error() { [native code] }

综上所述,constructor 对 underfined 和 null 无效(因为它们不是对象,不能从 Object.prototype 上继承 constructor 属性)

此外, constructor 的指针是可以改变的(因为它就是个属性,以下例子属于属性赋值)

function Person() {}
function Student() {}
Student.prototype = new Person();
var student = new Student();
console.log(student.constructor); // Person() {}

具体我们在原型一文中做详细介绍

Object.prototype.toString.call(source)

toString() 方法返回一个表示该对象的字符串

每个对象都有一个toString() 方法(继承自 Object.prototype 老祖),用它能真正做到对类型的检测

// 继续引用上述例子
// 基本类型
console.log(Object.prototype.toString.call(null1)); //[object Null]
console.log(Object.prototype.toString.call(undefined1)); //[object Undefined]
console.log(Object.prototype.toString.call(string1)); //[object String]
console.log(Object.prototype.toString.call(number1)); //[object Number]
console.log(Object.prototype.toString.call(boolean1)); //[object Boolean]
console.log(Object.prototype.toString.call(symbol1)); //[object Symbol]
// 引用类型
console.log(Object.prototype.toString.call(myString)); //[object String]
console.log(Object.prototype.toString.call(myNumber)); //[object Number]
console.log(Object.prototype.toString.call(myBoolean)); //[object Boolean]
console.log(Object.prototype.toString.call(myObject)); //[object Object]
console.log(Object.prototype.toString.call(myArray)); //[object Array]
console.log(Object.prototype.toString.call(myFunction)); //[object Function]
console.log(Object.prototype.toString.call(myDate)); //[object Date]
console.log(Object.prototype.toString.call(myRegExp)); //[object RegExp]
console.log(Object.prototype.toString.call(myError)); //[object Error]

我们可以看到它返回 [object NativeConstructorName] 格式的字符串,它能清晰的判断我们所需要的原生构造函数。同样,它的缺点是不能检测非原生构造函数

在 jquery 中的$.type() 是内部的原理用的就是 Object.prototype.toString.call(),让我们手写一个类型判断的小小工具库吧

function isType(source) {
    const target = Object.prototype.toString.call(source);
    switch(target) {
        case "[object Null]":
            return 'null';
        case "[object Undefined]":
            return 'undefined';
        case "[object String]":
            return 'string';
        case "[object Number]":
            return 'number';
        case "[object Boolean]":
            return 'boolean';
        case "[object Object]":
            return 'object';
        case "[object Array]":
            return 'array';
        case "[object Function]":
            return 'function';
        case "[object Date]":
            return 'date';
        case "[object RegExp]";
            return 'regexp';
        case "[object Error]";
            return 'error'
    }
}
function getType(target) {
    return Object.prototype.toString.call(target);
}

这里你或许会感到疑惑,Object.prototype 的 constructor 不能调用 null、undefined,而Object.prototype.toString 却能调用。这是为什么?

这里牵扯到隐式原型继承,即用对象字面量时,就已经实现了继承,而Object.prototype.toString 用的是原始方法,不信,你看

console.log(null1.toString()); // Cannot read properties of null (reading 'toString')
console.log(undefined1.toString()); // Cannot read properties of undefined (reading 'toString')

总结

这一节我们讲了 JavaSript 由什么组成

从组成上分,它包括语法,变量和数据类型,关键字和保留字,操作符,语句,对象

其中数据类型包括基本类型(简单类型/值类型)和引用类型(复杂类型)。基本类型有 number、string、boolean、null、undefined 、symbol、bigInt,引用类型则是 object

那如何判断数据类型呢?笔者总结四种方法

名称 能检测 不能检测
typeof string、number、boolean、undefined 以及 function null 以及除 function 外的对象,结果都为 object
instanceof 准确地判断复杂引用数据类型 不能正确判断基础数据类型
constructor string、number、boolean、array、object、function 以及 构造函数 undefined、null。不安全,因为指向可以改变
Object.prototype.toString() 内置(原生)构造函数 自定义构造函数

我们了解了基本类型、引用类型,它们的区别以及如何区分它们,但基本类型是简单的,而引用类型虽然只有 object,但它却在 JavaScript 占了大部分知识点,只有掌握 object,才算学会 JavaScript

下一篇,我们讲讲 JavaScript 中的 KING—— 对象

系列文章

  • 深入理解JavaScript-开篇
  • 深入理解JavaScript-JavaScript 是什么