前言
今天又又又...又整理了一下,那些 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
判断以上数据类型的话题,老生常谈了
原始类型的比较的是值,只有两者的值相等,那么它们被认为是相等的,否则不相等。而引用类型比较的是地址,当两者的标识符同时指向内存的同一个地址,则被认为是相等的,否则不相等。
console.log({} == {}) // false console.log([] == []) // false
二、原始类型与原始值
所有基本类型的值(即原始值,Primitive Values)都是不可改变(immutable)的,而且不含任何属性和方法的。
到这里可能会有小伙伴打问号了???
Q1:原始类型与原始值有什么区别?
原始类型的值称为原始值。例如原始类型 Boolean 有两个(原始)值
true
和false
。同样的原始类型 Undefined(Null),只有一个原始值undefined
(null
)。其他的就有很多个了...
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 中除了 null
和 undefined
之外,所有的基本类型都有其对应的包装对象(Wrapper Object)。因此,访问 null
或 undefined
的任何属性和方法都会抛出错误。
String
为字符串基本类型。Number
为数值基本类型。BigInt
为大整数基本类型。Boolean
为布尔基本类型。Symbol
为字面量基本类型。
这些包装对象的 valueOf
方法返回其对应的原始值。
再次明确一点,原始值是没有任何属性和方法的。
不是说好的,原始值不含任何的属性和方法吗?那 foo.length
和 foo.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"
Object
和 Function
本身就是 JavaScript 中自带的函数对象。其中 obj1
、obj2
、obj3
为普通对象(均为 Object
的实例),而 fn1
、fn2
、fn3
为函数对象(均是 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]]
是对象的一个内部属性,其值为以下字符串之一:
- 常见的有:
Function
、Object
、Array
、Boolean
、Number
、String
、Symbol
、RegExp
、JSON
、Date
、Math
、Error
、Arguments
等。 - 比较少用的有:
BigInt
、Set
、WeakSet
、Map
、WeakMap
、Reflect
、Promise
、GeneratorFunction
、AsyncFunction
、Window
、Intl
、WebAssembly
,以及派生于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 类型转换的内容,已经单独写了一篇文章详细地介绍了
未完待续...