类型判断
我们首先来看看 JS 中有多少数据类型。JS 中数据类型分为原始类型(也叫基础数据类型)和引用类型,我们先来看原始类型:
1. 原始类型
有 string、 number、 boolean、 undefined、 null、 symbol、 bigint 七种。
这些原始类型构成了 JavaScript 数据处理的基础单元,每一种类型都有其独特的特性和用途。例如,字符串用于表示文本信息,数字用于数值运算,布尔值用于逻辑判断等。
2. 引用类型
主要有 object、 array、 function、 Data、 RegExp 几种类型。
引用类型则相对复杂一些,它们可以包含多个属性和方法,并且在内存中的存储方式与原始类型有所不同。对象可以用来组织和存储相关的数据,数组用于存储一组有序的数据,函数则是可执行的代码块等等。
JavaScript中有多少数据类型------有八种,七种原始类型和对象。
3. 类型判断
为什么我们需要类型判断?我们先来看下面这个代码,一个简单的两数相加方法:
function add(x, y) {
return x + y;
}
console.log(add('2', 3));
我们在这里给 add 函数两个实参,字符串 '2' 和数字 3,那这里会输出什么?
这里就会把字符串拼接起来,把 2 和 3 拼在一起,输出 23。
那我们原本这个函数是对两个实参进行相加的,因为我们这里没有判断输入的是什么类型,所以使代码发生了类型强制转换导致的意外结果错误。
由此可见需要类型判断来确保运算结果正确、避免运行时错误以及增强代码可维护性与可读性。
当然,我们这里也可以对输入的实参进行类型转换,转成我们希望的 number 类型:
//如果用户传进来的是字符串,进行
//要进行判断
function add(x, y) {
return Number(x) + Number(y); //转换成数字再相加(显示类型转换)
}
console.log(add('2', 3));
//‘hello’,NAN,是number类型(无法表达的数字)
输出正确了,但是如果我们这里输入的是字符串 'hello' 呢?这里就不展示运行结果了,输出会是 NaN,
这是治标不治本的方法,其实我们不希望输入字符串,所以我们这里最好还是进行类型判断。
下面我们来讲述几种类型判断的方法:
1. typeof
我们这里分成两个类型来实验,原始类型和引用类型:
console.log(typeof 'hello'); // string
console.log(typeof 123); // number
console.log(typeof undefined); // undefined
console.log(typeof Symbol(1)); // symbol
console.log(typeof 111n); // bigint
console.log(typeof null); // object,唯独无法准确判断,object是通过二进制判断,null是一串0
//引用类型,几乎全是object,判断的不准确,引用类型的二进制前三位是0
console.log(typeof {}); // object
console.log(typeof []); // object
console.log(typeof new Date); // object
console.log(typeof function() {}); // function
来看看执行结果:
所以:
typeof 可以准确判断除了 null 之外的所有原始类型,不能判断引用类型(除了function)
2. instanceof
同样分成两个类型,原始类型和引用类型:
// {} 是普通对象,其隐式原型(__proto__)指向Object构造函数的显式原型(Object.prototype),所以通过instanceof判断为Object类型的实例,输出 true。
console.log({} instanceof Object);
// [] 为数组,它的隐式原型(__proto__)指向Array构造函数的显式原型(Array.prototype),因此用instanceof判断是Array类型实例,输出 true。
console.log([] instanceof Array);
// new Date() 生成的日期对象,其隐式原型(__proto__)指向Date构造函数的显式原型(Date.prototype),故instanceof判断为Date类型实例,输出 true。
console.log(new Date() instanceof Date);
// function() {} 这个匿名函数的隐式原型(__proto__)指向Function构造函数的显式原型(Function.prototype),所以instanceof判断是Function类型实例,输出 true。
console.log(function() {} instanceof Function);
//==================================================
// 'hello' 是原始类型字符串字面量,不是由String构造函数创建的实例,不存在隐式原型指向String构造函数显式原型的情况,所以instanceof判断不是String类型实例,输出 false。
console.log('hello' instanceof String);
// 123 是原始类型数字字面量,并非通过Number构造函数创建的实例,无隐式原型与Number构造函数显式原型的关联,instanceof判断不是Number类型实例,输出 false。
console.log(123 instanceof Number);
// true 是原始类型布尔值字面量,不是通过Boolean构造函数创建的实例,不存在相应隐式原型与显式原型关联,instanceof判断不是Boolean类型实例,输出 false。
console.log(true instanceof Boolean);
console.log(true instanceof Boolean);
我们这里来看看原始类型 null:
//那Null要不要大写?有构造函数,首字母大写。null、 undefined 没有构造函数
console.log(null instanceof null);
为什么会报错?
因为null 是一个特殊的值,它表示空值,没有原型链。在 JavaScript 中,null 不是一个对象,虽然 typeof null 返回 "object"(这是 JavaScript 早期的一个历史遗留问题)。
当你写 null instanceof null 时,instanceof 操作符期望左边是一个有原型链的对象,而 null 没有原型链,无法按照 instanceof 的规则进行检查,所以会抛出一个类型错误(TypeError)。
==========================================================
大家来思考一下,用这个来判断数组 [] 是不是对象 Object,会输出什么?
console.log([] instanceof Object);
结果是 true。
来捋一捋怎么判断的:
- 当执行
[] instanceof Object时,instanceof操作符会沿着[]的原型链进行查找。 - 它首先会检查
[]的隐式原型(__proto__),也就是Array.prototype,发现与Object.prototype不匹配。 - 然后继续查找
Array.prototype的隐式原型(Array.prototype.__proto__),此时发现它等于Object.prototype,所以[] instanceof Object返回true。这意味着从原型链的角度来看,数组[]也是Object类型的一个衍生对象,因为它的原型链最终可以追溯到Object.prototype。
==========================================================
这段代码先定义 Car 函数并赋予实例 run 属性,再通过 Bus.prototype = new Car(); 实现 Bus 对 Car 的原型链继承, Bus 函数有 name 属性。bus 作为Bus 实例,其原型链可访问Car属性,这里用 instanceof 来判断 bus 是否是 Car 、 Object构造函数对应的实例。
function Car() {
this.run = 'running'
}
Bus.prototype = new Car();
function Bus() {
this.name = 'BYD';
}
let bus = new Bus();
console.log(bus instanceof Bus); //我的隐式原型等于你的显式原型bus.__proto__== Bus.prototype
console.log(bus instanceof Car); //bus.__proto__.__proto__ == Car.prototype
console.log(bus instanceof Object);
输出:
我们这里来解释一下这三个 true:
bus的隐式原型(__proto__)指向Bus.prototypebus的隐式原型(__proto__)指向Bus.prototype,而Bus.prototype(作为Car的实例)的隐式原型(Bus.prototype.__proto__)指向Car.prototype。- 在 JavaScript 中,所有对象的原型链最终都会指向
Object.prototype。对于bus这个对象,它的原型链(先bus.__proto__指向Bus.prototype,Bus.prototype.__proto__又会继续向上追溯)最终也会指向Object.prototype。
Over,最后来总结一下:
instanceof依据原型链判断类型是否相等,即检查对象的隐式原型是否与构造函数的显式原型匹配,若匹配则表明该对象是此构造函数的实例,从而确定类型相等关系。但只能判断引用类型,不能判断原始类型(因为原始类型没有隐式类型)
3. Object.prototype.toString.call(x)
那有没有一个方法,既能判断原始类型又能判断引用类型?
没错,就是 Object.prototype.toString.call(x)。
我们先来试试 Number 类型:
let a = 1
console.log(Object.prototype.toString.call(a));//把 obj 原型上的 toString 的方法指到 a 身上来
判断正确。那这个判断原理是什么呢?
让我们来看看 js 官方文档中,Object.prototype.toString() 的规则
ES5
- 如果 this 值为 undefined,返回 "[object Undefined]"。
- 如果 this 值为 null,返回 "[object Null]"。
- 设 O 为调用 ToObject 的结果,将 this 值作为参数传递 ToObject(this),
- 设 class 为 O 的 [[Class]] 内部属性的值。 // 得到了 O 的类型,
- 返回由 “[object ”、 class 和 “]” 三块拼接的结果。
如果没有call的话,那里面的this会指向谁呢?我们来试验一下:
let a = 1
console.log(Object.prototype.toString(a));
答案是全局对象
这里可能有几点疑问
1. 为什么 this 不是指向 Object的实例对象呢?
之前我们说过:构造函数原型上的this指向实例对象。
但这里 Object.prototype.toString 是作为一个普通函数调用,当你直接调用 Object.prototype.toString(a) 时,它是作为普通函数执行的,而不是通过构造函数创建的实例调用的。因此,this 并不指向实例对象。
2. 这是非独立调用吗?
这是非独立调用,因为这里没有通过 call() 或 apply() 来显式设置 this,而是直接调用了 Object.prototype.toString,它根据上下文决定 this 的值。
OK,解决了一些疑问,那我们这里来回顾一下 call。
//回顾一下call
function test(){
console.log(this.a);
}
let obj = {
a:1
}
test.call(obj)
//1. 让 obj 拥有 test
//2. obj.test()
//3. delete obj.test
//call可以把test借给obj用
回顾完再回去看代码,是不是就知道为什么要使用 call 了。
Object.prototype.toString.call(x)借助 Object 原型上的toString方法在执行过程中会读取x的内部属性 [[class]] 这一机制。
call是函数的一个内置方法,它允许我们显式地指定函数内部this的指向,并调用该函数。在这里使用
call(a),就是将Object.prototype.toString这个函数执行时的this指向了变量a所代表的对象或值(在进行相关判断时原始类型会被临时包装成对应的包装类)。
例如,如果 a 是一个普通的数字字面量(如代码中 let a = 1; 这样的情况),虽然它本身是基本数据类型,但在执行 Object.prototype.toString.call(a) 时,JavaScript 会自动将其临时包装成 Number 类型的对象来进行判断(也就是后面要讲的包装类),最终会返回 "[object Number]" 这样的字符串,从而让我们清楚地知道它对应的类型情况。
再比如,如果 a 是一个数组 let a = []; ,那么执行 Object.prototype.toString.call(a) 就会返回 "[object Array]" ,能精准识别出它是数组类型。
Object.prototype.toString.call(x) 借助Object原型上的toString方法在执行过程中会读取 x 的内部属性[[class]]这一机制
4. Array.isArray(x)
此方法专门用于判断一个对象是否为数组类型,在处理数组相关的逻辑时非常实用,可以快速准确地确定一个变量是否为数组,避免了使用其他通用类型判断方法可能带来的误判。
let arr = []
console.log(Array.isArray(arr));
//数组身上的方法
包装类
首先来看看下面的代码会输出什么?
console.log('hello' instanceof String);
console.log(new String('hello') instanceof String);
:|
为什么结果会不一样,不都是字符串 "hello" 吗?
- 因为第一个是包装类,第二个是字面量(没有属性方法)
instanceof看对象的原型链上有没有构造函数的prototype。- 基本字符串
'hello'没有,所以'hello' instanceof String为false;new String('hello')有,所以new String('hello') instanceof String为true。
那我们看下一个问题:
let num = 123
num.a = 1
console.log(num.a);
为什么不会报错?先不急着解答,再看一个代码
let str = 'abcde'
console.log(str.length);
输出是什么呢?
如果没有之前的代码,你肯定会说,会输出一个字符串的长度。让我们来看看
可能现在脑袋有点转不过来,解答一下:
这边用注释来描述一下 v8 引擎在做什么。
let num = 123 //let num = new Number(123)(js 中所有创建的字面量,在执行过程中都是这样)
num.a = 1 //真的往 num 上添加了key 为 a,值为 1,后面有解释
console.log(num.a);
//读取值时会触发一个机制:原始类型不能拥有属性和方法,属性和方法只能是引用类型的
//不对!用户想要的是字面量,必须满足用户的要求------把实例对象Number(123)转变成字面量
//读取[[PermitiveValue]] ,下面有介绍
//delete num.a
//输出 undefined
这第二行 obj.a 怎么理解呢?
答案揭晓:既有往 obj 增加一个属性 a,也有读取 obj.a 的值(undefined)。但是我们在 obj 里没有看到属性 a ,是因为没有赋值所以移除掉了。
所有的包装类里面都有一个这样的属性 [[PermitiveValue]] ,我们读取不到。(两个中括号的属性,不是我们能用的,而是v8 引擎内部使用的)
那我们回归到本节的第二个代码:
let str = 'abcde' // let str = new String('abcde')
console.log(str.length);//读的是包装类上的属性,从包装类的`prototype`属性所指向的对象(即`String.prototype`)中获取`length`属性的值
//同之前的 123 一样
str.len = 2
console.log(str.len); //读的是字面量上面的属性 len
也是 undefined。
来看看如果是构造函数 new 出来的实例对象:
let str2 = new String('length')//用户想要的是对象
str2.len = 2
console.log(str2.len);
所以:
- 原始类型不能拥有属性和方法,属性和方法只能是引用类型的
- 访问对象上不存在的属性会得到 underfined 而不会报错
5. 练习
仿写一个类型判断方法
//这里提供一个思路,顺着原型链
function myinstanceof(L, R) {
if (L.__proto__ === R.prototype) {
return true
}else{
if (L.__proto__.__proto__ === R.prototype) {
return true
}else{
return false
}
//...
}
}
这里有两种解法:
//循环
function myinstanceof(L, R) {
while (L !== null) {
L = L.__proto__
if (L === R.prototype) {
return true
}
}
return false
}
//递归
function myinstanceof(L, R) {
L = L.__proto__
if (L === R.prototype) {
return true
}else{
return myinstanceof(L, R)
}
}