简介
由于在面试经常会遇到问题null,undefined,NaN之间的区别,因此想要深入且系统了解一下这些代表空之间的区别,以及它们底层的原理——原型链,同时还要搞明白__proto__
和prototype
分别是什么。
为了更好系统的理解null,undefined,NaN之间的区别和关系,我们需要从Javascript语言设计底层去理解,为什么一个空值需要设计这么多个。为什么不能像Java,一个null就可以满足?
我们先简单认识三者:
undefined
表示原始值undefined。它是一个 JavaScript 的 原始数据类型null
特指对象的值未设置。它是 JavaScript 基本类型 之一。NaN
是一个表示非数字的值
接下来主要从以下两个点去认识null,undefined,NaN之间的区别和关系:
- 数据类型
typeof
===
判断- 原型
prototype
数据类型
在Javascript中,typeof
可以判断出当前变量的数据类型,主要以下几种数据类型:
typeof undefined
,输出undefined
typeof NaN
,输出number
typeof null
输出object
从上面的结果可以得知 undefined
在JavaScript中是一种数据类型,而NaN
和null
则是某种数据类型的值。
但是在JS在定义基础数据类型有以下集中种:
number
数字类型,包括数字 和 NaNstring
字符串类型boolean
布尔类型 包括: true 和 falseundefined
undefined未定义类型null
null空数据类型bigint
ES2020新定义 BigInt大整数类型,主要用来解决大于 2^53 - 1 的整数,如:const theBiggestInt = 9007199254740991n; const alsoHuge = BigInt(9007199254740991);
symbol
ES6定义 Symbol类型,应用场景:解决属性命名唯一性的问题,比如一个object里有两个属性名都一样,但是分别对不同的意思,可以通过Symbol类型去解决,如:a = Symbol('test'); b = Symbol('test');obj = {[a]: 'test', [b]: 'test'}
同时typeof运算符还可以返回以下两种类型:
function
函数类型,新的class
也是返回function
object
除了以上类型,其他对象统一返回object
因此 null
被单独归类成一种基础数据类型,但是为什么typeof null
得到的却是 object
?
typeof
先了解一下typeof是什么?
typeof是一个操作符而不是函数,用来检测给定变量的数据类型。
typeof是一个操作符,和 +
-
=
符号一样,只是用了typeof
字母进行标识而已,类似还有:delete
void
in
instanceof
等。
使用typeof需要注意以下几点:
1.typeof 操作符的优先级高于加法(+)等二进制操作符。因此,需要用括号来计算加法结果的类型。 如:typeof someData + " Wisen"; // "number Wisen"
typeof (someData + " Wisen"); // "string"
2.typeof 通常总是保证为它提供的任何操作数返回一个字符串。即使使用未声明的标识符,typeof 也会返回 "undefined",而不是抛出错误。 但是在let
const
声明的变量,使用 typeof 会抛出一个 ReferenceError。因为let
const
声明的变量块作用域变量在块的头部处于“暂存死区”。
3.typeof document.all === 'undefined'; // true
因为所有浏览器都公开了一个类型为 undefined 的非标准宿主对象 document.all
,但是 document.all
不等于 undefined
,这种情况出现是在 Web 标准中,document.all 具有 "undefined" 类型的情况被归类为“故意违反”原始 ECMAScript Web 兼容性标准。
4.typeof 并不能检查出所有的类型,只能检查出上述所说的8种,针对其他类型可以通过原型链去判断获取,如: Object.prototype.toString.call(x)
x.constructor.name
typeof 工作原理
那么typeof真正是如何工作的呢?
- 首先,会将所运算的变量数据在底层转换成二进制,而在Javascript设计中,是利用二进制前(低)三位存储其类型信息,如:000: 对象,1:整数, 100:字符串等
- 其次,null存储起来转成二进制为
0000000000000000
,那么按照JS的设计原则, 低三位为000
则代表对象 - 因此,null在typeof计算后,会直接返回
object
PS: 注意细节, undefined
:用 - (−2^30)表示。
这里引用一下迷渡 justjavac老师的几个解释:
JavaScript中typeof原理探究?v8引擎是如何知道js数据类型的?
===运算符
要讲三者的区别,还有一种方式就是通过 ===
全等运算符,它们的表现如下:
null === null // true
undefined === undefined // true
NaN === NaN // false
前两者比较好理解,毕竟都是等于自身,但是NaN
不等于NaN
就很容易给人带来误解。
这里就需要先解释===
和==
的区别:
- ==, 两边值类型不同的时候,要先进行类型转换,再比较两者的值。
- ===,不做类型转换,类型不同的一定不等,然后对比两者的值。
因此我们可以得到===
的实现过程:
- 先判断两者的数据类型是否一致
- 再判断两者是否为基础数据类型,如果是数字类型还需要判断两者是否有为NaN,如果没有返回true,其他基础类型直接对比值
- 如果是引用数据类型,如:object或function,则判断它们的引用对象是否为同一个,如:
a = function(){}; b= a; c=a; b===c;
NaN
NaN 即 Not a Number ,代表该值不是一个数字number类型
那么如何判断NaN
值呢?答案是通过isNaN(x)
或者Number.isNaN(x)
函数去判断,这里也有坑就是Number.isNaN(x)
不会强制转换x
为number
,只是会判断x是否为NaN
为什么JS要设计一直NaN
值? 个人猜测是因为JS是一门弱类型语言,它支持类型之间互相转换,其他基础数据类型都可以直接转换或者报类型错误,但是在number
为了更好兼容加减乘除运算符,设计一个值为NaN(非数字),当值无法转换number
类型,将返回NaN。
同时,我们也需要了解NaN
与Infinity
的区别:
- NaN,是Not a Number的缩写,不是一个数字的意思。
- Infinity,是指无穷大的数字,后面可以用BigInt数据类型代替。
原型
如果要了解到三者的本质,从三者的原型去区分:
Object.prototype.toString.call(undefined) // [object Undefined]
,原型为自己本身或者没有原型,因为它是一个基础数据类型,且是全局对象(window)的一个属性,并不是一个实例化的对象Object.prototype.toString.call(null) // [object Null]
, 原型为自己本身,因为它是原型链上的最后一个Object.prototype.toString.call(NaN) // [object Number]
,原型为Number
,因为它是属于Number类型
那么什么是原型呢?
我们需要明白原型是怎么出现的,是为了解决什么问题?
原型机制,是JS语言实现面向对象编程中继承特性是设计的一种机制,这种继承机制与经典的面向对象编程语言的继承机制不同。 传统的面向对象编程,实现对象继承,通过是定义一个父类,如果有个子类继承父类,那么在子类在实例化,会将父类的属性和方法都复制一份到子类的实例中 而原型机制是每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推,最终形成
原型链 (prototype chain)
。 这些属性和方法定义在 Object 的构造器函数 (constructor functions) 之上的prototype属性上,而非对象实例本身
如何理解呢?下面通过一下几个点:
- new一个对象的全过程
- 如何实现继承?以及继承中的原型链?
但是在搞清楚这个之前,我们还需要了解JS原型中经常会混淆两个属性,prototype
和__proto__
,所以我们先弄明白这两个属性分别是做什么的。
prototype和__proto__
在JS里,万物皆对象。方法(Function)是对象,方法的原型(Function.prototype)是对象。因此,它们都会具有对象共有的特点。 那么只要是对象就会有属性
__proto__
,称为隐式原型,一个对象的隐式原型指向构造该对象的构造函数的原型对象prototype
。
为了更好的理解,我们通过一段代码和对应原型关系图去对比了解:
A = function(){}; a = new A();
上述代码prototype
和__proto__
的关系如下图:
通过上图我们可以很清晰的知道两者的区别:
prototype
是一个对象,只有函数才有,实例化后的变量是没有的,且prototype
原型允许扩展函数的方法或者属性,从而让实例化后的对象进行使用,再者就是prototype
既然是对象那么它自己也会有__proto__
属性__proto__
是实例化对象后拥有的属性,它的值主要指向该对象构造函数的原型prototype
,从而形成原型链
- 当一个实例化对象在调用某方法或某属性时,会先判断
__proto__
的prototype
对象上是否有,如果没有则会往下一层__proto__
去寻找
new的实现过程
demo代码如下:
A = function(){}; a = new A();
那么在这一个过程中,js在底层中,做了哪些事情呢? 通过上图我们可以很清楚的知道:
- 创建全新的函数A实例化一个对象
- 然后将对象的__proto__指向构造函数的prototype
- 将对象的this指向到调用方的this
- 如果函数无返回对象类型Object,则返回该函数对象
继承与原型链
继承其实在ES6后来说已经很简单了,因为定义了class
和extends
等语法糖,所以不需要再像之前通过原型去解决,但是为了更好的了解原型链,我们接下来尝试一下几种ES5时代实现继承的方式。
原型指向实现
function A(){ this.a = 'test'; } A.prototype.say = function(){ console.log(this.a); } function B(){ // 需要将A的属性继承 A.call(this); } B.prototype.__proto__ = A.prototype; // 思考一下,为什么要将隐性原型指向A? B.__proto__ = A; var b = new B(); b.a; // test b.say(); // test b instanceof B; // true b instanceof A; // true
这里需要注意几个点:
1.在B构造函数里实现A.call(this)
,是因为需要将A函数实现一遍,且A中this指向的属性绑定B函数中
2.B.prototype.__proto__ = A.prototype
这行是继承A的方法,后续如果要重写相同方法需要放在这行代码后面
3.B.__proto__ = A
如果没有这行代码,貌似上面的结果并不会有太多异常,那么为什么要这行代码呢?(等待后续解释)
总结
这里再总结一下,在数据类型上,三者的区别主要是:
undefined
是一种基础数据类型,可以通过typeof
直接识别null
虽然也是一种基础数据类型,但是由于typeof
从一开始实现机制问题,typeof null
一直遗留下来返回的object
NaN
是number
数据类型的一个值,代表无法识别为数字类型的值,如:Number('abc') // NaN
在 ===
运算符号上,区别主要是:
undefined
,可以等于任何值为undefined
的变量null
,可以等于任何值为null
的变量NaN
,不等于任何值的变量,只能通过isNaN
函数判断
从原型上去了解三者,他们本质就完全不同:
undefined
,原型为自己本身或者没有原型null
,原型为自己NaN
,原型为Number
同时我们也清楚的认识到JS中原型和原型链,原型是JS设计实现面向对象的一种机制,主要通过两个东西实现原型对象prototype
和隐性原型__proto__
实现,这两者主要关系在于:
- 隐性原型
__proto__
是一个实例化对象后的属性,会指向声明该对象的构造函数的原型对象prototype
- 原型对象
prototype
是一个对象,一般只有函数才会有,它通常包括constructor
构造函数指向函数本身,以及其他扩展方法和属性,同时它本身也拥有__proto__
隐性原型属性