从null、undefined、NaN的区别了解JS的原型链

简介: 由于在面试经常会遇到问题null,undefined,NaN之间的区别,因此想要深入且系统了解一下这些代表空之间的区别,以及它们底层的原理——原型链,同时还要搞明白__proto__和prototype分别是什么。

简介


由于在面试经常会遇到问题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中是一种数据类型,而NaNnull则是某种数据类型的值。


但是在JS在定义基础数据类型有以下集中种:

  • number 数字类型,包括数字 和 NaN
  • string 字符串类型
  • boolean 布尔类型 包括: true 和 false
  • undefined 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字母进行标识而已,类似还有:deletevoidininstanceof等。

使用typeof需要注意以下几点:

1.typeof 操作符的优先级高于加法(+)等二进制操作符。因此,需要用括号来计算加法结果的类型。 如:typeof someData + " Wisen"; // "number Wisen" typeof (someData + " Wisen"); // "string"

2.typeof 通常总是保证为它提供的任何操作数返回一个字符串。即使使用未声明的标识符,typeof 也会返回 "undefined",而不是抛出错误。 但是在letconst声明的变量,使用 typeof 会抛出一个 ReferenceError。因为letconst声明的变量块作用域变量在块的头部处于“暂存死区”。

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) 不会强制转换xnumber,只是会判断x是否为NaN

为什么JS要设计一直NaN值? 个人猜测是因为JS是一门弱类型语言,它支持类型之间互相转换,其他基础数据类型都可以直接转换或者报类型错误,但是在number为了更好兼容加减乘除运算符,设计一个值为NaN(非数字),当值无法转换number类型,将返回NaN。

同时,我们也需要了解NaNInfinity的区别:

  • 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__的关系如下图:

006fcb30369cca6425e7b019c744899.png

通过上图我们可以很清晰的知道两者的区别:

  • prototype是一个对象,只有函数才有,实例化后的变量是没有的,且prototype原型允许扩展函数的方法或者属性,从而让实例化后的对象进行使用,再者就是prototype既然是对象那么它自己也会有__proto__属性
  • __proto__是实例化对象后拥有的属性,它的值主要指向该对象构造函数的原型prototype,从而形成原型链
  • 当一个实例化对象在调用某方法或某属性时,会先判断__proto__prototype对象上是否有,如果没有则会往下一层__proto__去寻找

new的实现过程


demo代码如下:

A = function(){};
a = new A();

那么在这一个过程中,js在底层中,做了哪些事情呢? 通过上图我们可以很清楚的知道:

  • 创建全新的函数A实例化一个对象
  • 然后将对象的__proto__指向构造函数的prototype
  • 将对象的this指向到调用方的this
  • 如果函数无返回对象类型Object,则返回该函数对象

继承与原型链


继承其实在ES6后来说已经很简单了,因为定义了classextends等语法糖,所以不需要再像之前通过原型去解决,但是为了更好的了解原型链,我们接下来尝试一下几种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
  • NaNnumber数据类型的一个值,代表无法识别为数字类型的值,如:Number('abc') // NaN


=== 运算符号上,区别主要是:

  • undefined,可以等于任何值为undefined的变量
  • null,可以等于任何值为null的变量
  • NaN,不等于任何值的变量,只能通过isNaN函数判断


从原型上去了解三者,他们本质就完全不同:

  • undefined,原型为自己本身或者没有原型
  • null,原型为自己
  • NaN,原型为Number


同时我们也清楚的认识到JS中原型和原型链,原型是JS设计实现面向对象的一种机制,主要通过两个东西实现原型对象prototype和隐性原型__proto__实现,这两者主要关系在于:

  • 隐性原型__proto__是一个实例化对象后的属性,会指向声明该对象的构造函数的原型对象prototype
  • 原型对象prototype是一个对象,一般只有函数才会有,它通常包括constructor构造函数指向函数本身,以及其他扩展方法和属性,同时它本身也拥有__proto__隐性原型属性
目录
相关文章
|
7天前
|
存储 JavaScript 索引
js开发:请解释什么是ES6的Map和Set,以及它们与普通对象和数组的区别。
ES6引入了Map和Set数据结构。Map的键可以是任意类型且有序,与对象的字符串或符号键不同;Set存储唯一值,无重复。两者皆可迭代,支持for...of循环。Map有get、set、has、delete等方法,Set有add、delete、has方法。示例展示了Map和Set的基本操作。
19 3
|
8天前
|
JavaScript 前端开发
js开发:请解释原型继承和类继承的区别。
JavaScript中的原型继承和类继承用于共享对象属性和方法。原型继承通过原型链实现共享,节省内存,但不支持私有属性。
17 0
|
2天前
|
前端开发 JavaScript
【Web 前端】 js中call、apply、bind有什么区别?
【4月更文挑战第22天】【Web 前端】 js中call、apply、bind有什么区别?
【Web 前端】 js中call、apply、bind有什么区别?
|
2天前
|
前端开发 JavaScript
【Web 前端】undefined 和 null 区别?
【4月更文挑战第22天】【Web 前端】undefined 和 null 区别?
【Web 前端】undefined 和 null 区别?
|
2天前
|
存储 前端开发 JavaScript
【Web 前端】JS数据类型有哪些?区别?
【4月更文挑战第22天】【Web 前端】JS数据类型有哪些?区别?
|
4天前
|
JavaScript 前端开发
js的let、const、var的区别以及应用案例
【4月更文挑战第27天】ES6 中,`let` 和 `const` 是新增的变量声明关键字,与 `var` 存在显著差异。`let` 允许重新赋值,而 `const` 不可,且两者都具有块级作用域。`var` 拥有函数级作用域,并可在函数内任意位置访问。`let` 和 `const` 声明时必须初始化,而 `var` 不需。根据需求选择使用:局部作用域用 `let`/`const`,全局或函数范围用 `var`,不可变值用 `const`。
14 2
|
6天前
|
JSON JavaScript 前端开发
vue2_vite.config.js的proxy跨域配置和nginx配置代理有啥区别?
vue2_vite.config.js的proxy跨域配置和nginx配置代理有啥区别?
21 1
|
6天前
|
JavaScript 前端开发 Oracle
java和JavaScript的区别
java和JavaScript的区别
7 3
|
8天前
|
JavaScript 前端开发
js开发:请解释同步和异步编程的区别。
同步编程按顺序执行,易阻塞;异步编程不阻塞,提高效率。同步适合简单操作,异步适合并发场景。示例展示了JavaScript中同步和异步函数的使用。
17 0
|
9天前
|
存储 缓存 前端开发
< 今日份知识点:Javascript本地存储的方式有哪些?区别及应用场景? >
在前端开发中,偶尔需要存储一些如: 用户信息、登录状态、历史记录等常量数据。用于后续二次调用,并且避免刷新后丢失。这时,就需要用到本地存储了。 在`JavaScript` 中,提供了四种可用的本地存储方式: **`cookie`** ,**`sessionStorage`**, **`localStorage`**, **`indexedDB`** ( 已废除的 `WebSQL` )。四种方式各有千秋,接下来,就由小温带各位卷王了解一下,`Javascript` 中的本地存储吧
< 今日份知识点:Javascript本地存储的方式有哪些?区别及应用场景? >