从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__隐性原型属性
目录
相关文章
|
3月前
|
JavaScript 前端开发
JavaScript如何判断变量undefined
JavaScript如何判断变量undefined
|
2月前
|
JavaScript 前端开发 开发者
理解JavaScript中的原型链:基础与实践
【10月更文挑战第8天】理解JavaScript中的原型链:基础与实践
|
1月前
|
JavaScript 前端开发
js中的bind,call,apply方法的区别以及用法
JavaScript中,`bind`、`call`和`apply`均可改变函数的`this`指向并传递参数。其中,`bind`返回一个新函数,不立即执行;`call`和`apply`则立即执行,且`apply`的参数以数组形式传递。三者在改变`this`指向及传参上功能相似,但在执行时机和参数传递方式上有所区别。
27 1
|
1月前
|
JavaScript 前端开发
JavaScript 原型链的实现原理是什么?
JavaScript 原型链的实现原理是通过构造函数的`prototype`属性、对象的`__proto__`属性以及属性查找机制等相互配合,构建了一个从对象到`Object.prototype`的链式结构,实现了对象之间的继承、属性共享和动态扩展等功能,为 JavaScript 的面向对象编程提供了强大的支持。
|
1月前
|
设计模式 JavaScript 前端开发
js中new和object.creat区别
【10月更文挑战第29天】`new` 关键字和 `Object.create()` 方法在创建对象的方式、原型链继承、属性初始化以及适用场景等方面都存在差异。在实际开发中,需要根据具体的需求和设计模式来选择合适的方法来创建对象。
|
1月前
|
JavaScript 前端开发
原型链在 JavaScript 中的作用是什么?
原型链是 JavaScript 中实现面向对象编程的重要机制之一,它为代码的组织、复用、扩展和多态性提供了强大的支持,使得 JavaScript 能够以简洁而灵活的方式构建复杂的应用程序。深入理解和熟练运用原型链,对于提升 JavaScript 编程能力和开发高质量的应用具有重要意义。
|
1月前
|
JavaScript 前端开发
如何使用原型链继承实现 JavaScript 继承?
【10月更文挑战第22天】使用原型链继承可以实现JavaScript中的继承关系,但需要注意其共享性、查找效率以及参数传递等问题,根据具体的应用场景合理地选择和使用继承方式,以满足代码的复用性和可维护性要求。
|
3月前
|
前端开发 JavaScript UED
JavaScript防抖和节流的使用及区别
JavaScript防抖和节流的使用及区别
112 57
|
2月前
|
JavaScript 前端开发 开发者
探索JavaScript原型链:深入理解与实战应用
【10月更文挑战第21天】探索JavaScript原型链:深入理解与实战应用
39 1
|
2月前
|
存储 JavaScript 前端开发
JavaScript 数据类型详解:基本类型与引用类型的区别及其检测方法
JavaScript 数据类型分为基本数据类型和引用数据类型。基本数据类型(如 string、number 等)具有不可变性,按值访问,存储在栈内存中。引用数据类型(如 Object、Array 等)存储在堆内存中,按引用访问,值是可变的。本文深入探讨了这两种数据类型的特性、存储方式、以及检测数据类型的两种常用方法——typeof 和 instanceof,帮助开发者更好地理解 JavaScript 内存模型和类型检测机制。
115 0
JavaScript 数据类型详解:基本类型与引用类型的区别及其检测方法

热门文章

最新文章