1、typeof的检测机制
ps:参考资料:珠峰教育
我们都知道,在js中用来检测数据类型时,一般都是使用typeof进行检测。但是它是怎样进行检测的呢?还有为什么typeof null为对象呢?typeof 函数名为function呢?它又是怎样区分各个数据类型的呢?
console.log(typeof 10) //number
console.log(typeof 'str') //string
console.log(typeof undefiend) //undfiend
console.log(typeof null) //object
console.log(typeof {}) //object
console.log(typeof []) // object
console.log(typeof function() {}) //object
console.log(typeof true) //boolean
console.log(typeof Symbol()) // symbol
console.log(typeof 10n) //bigint
下面就来探讨探讨探讨typeof的运行机制吧!
- 所有的数据监测类型值,在计算机底层都是按照“64”位 的二进制值进行存储的(这个看的是自己电脑是64位还是32位)
- typeof 是按照二进制值进行检测的
- 二进制的前三位是0,认为是对象,然后再去看有没有实现call方法,如果实现了,返回‘function’,没有实现,则返回‘object’
- 所以null是64个0,typeof null 返回的是‘object’ 【局限性】
- 000 对象
- 1 整数
- 010 浮点数
- 100 字符串
- 110 布尔值
- 00000...... null
- -2*30 undefined
- ...... 所以这也就解释了为啥typeof null是object了吧!也解释了为啥typeof function() {}是function而不是object了吧!
我们可以输出一下function:
再看看对象的:
明白了吧!
ps:参考资料:珠峰教育~
2、隐式转换(number、string)
number的隐式转换
我们知道在js中,有时候我们不需要自己去手动将其他类型的值转换为数值类型,而浏览器会自动帮我们将它转为数值类型!
Number('12')
=> 12
Number('12px')
=> NaN
Number(null)
=> 0
Number(undefined)
=> NaN
console.log(3*'3')
=> 9
console.log([] == false);
=> 都转为数字 0==0 -> true
console.log(![] == false)
=> 先处理 ![] => false -> false == false => true
那这是怎样进行转换的呢?原始值类型就不说了,这里主要说引用类型的转换。
@将对象转为number有四个步骤:
- 先调用对象的
Symbol.toPrimitive这个方法。 - 没有这个方法,再调用对象的
valueOf方法(这个方法每个对象都有),目的是获取原始值。 - 没有原始值再调用
toString方法,转换为字符串。 - 最后再把字符串基于
Number方法转化为数字
例子1:
let time = new Date();
time[Symbol.toPrimitive]('number')
=> 1655627495628
Number(time)
=> 1655627495628
例子2:
let arr = [10];
console.log(Number(arr)) //10
步骤:
1、先调用对象的Symbol.toPrimitive这个方法。
let arr = [10]
arr['Symbol.toPromitive']
=> undefined
2、没有这个方法,再调用对象的valueOf方法(这个方法每个对象都有),目的是获取原始值。
但是数组没有原始值:
let arr = [10];
arr.valueOf()
=> [10]
+ 0: 10
+ length: 1
+ [[Prototype]]: Array(0)
3、没有原始值再调用toString方法,转换为字符串:
let arr = [10];
arr.toString()
=> '10'
4、最后再把字符串'10' 基于Number方法转化为数字 => 10
string的隐式转换
string的隐式转换,我们也主要说的是引用类型,原始值类型就是调用toString方法。
@将对象转为string有三个步骤:
- 先调用对象的
Symbol.toPrimitive这个方法。 - 没有这个方法,再调用对象的
valueOf方法(这个方法每个对象都有),目的是获取原始值。 - 没有原始值再调用
toString方法,转换为字符串。
一般需要转换为字符串的,就是“+”号了:
“+” 出现左右“两边”,其中一边是字符串,或者是某些对象:会以字符串拼接的规则处理
"+"出现在一个值的左边,转换为数字
let obj= {}
obj[Symbol.toPrimitive]
-> undefined
let obj= {}
obj.valueOf()
-> {}
obj.toString()
-> '[object Object]'
console.log(10 + '10') // '1010'
console.log(10 + new Number(10)) // 20
// new Number(10)[Symbol.toPrimitive] -> undefined
// new Number(10).valueOf() -> 10
// 10+10 = 20
console.log(10 + new Date())
// new Date()[Symbol.toPrimitive]('default') -> 'Sun Jun 19 2022 17:13:37 GMT+0800 (中国标准时间)'
// '10Sun Jun 19 2022 17:13:37 GMT+0800 (中国标准时间)'
console.log(10 + [10])
// [10][Symbol.toPrimitive] -> undefined
// [10].valueOf() -> [10] 没有原始值
// [10].toString() -> '10' => 10 + '10' -> '1010'
let result = 100 + true + 21.2 + null + undefined + 'Tencent' + [] + null + 9 + false;
-> 100+1
-> 101+21.2
-> 122.2+0
-> 122.2+NaN
-> NaN + 'Tencent'
-> 'NaNTencent'+[]
-> 'NaNTencent'+null
-> 'NaNTencentnull'+9
-> 'NaNTencentnull9' + false
-> 'NaNTencentnull9false'
3、parseInt([val],[radix])和parseFloat([val])
perseInt([val],[radix]):
-
[val] 必须是字符串,如果不是,要先隐式转换为字符串 String([val])
-
[radix]进制
- 如果不写,或者写0:默认是0(特殊情况:如果字符串是以0x开始的,默认值是16)
- 有效范围:2~36之间,如果不在这个区间,结果直接是NaN
-
从[val]字符串左侧第一个字符开始查找,查找出符合[radix]进制的值(遇到不符合的则结束查找,不论后面是否还有符合的);把找到的内容,按照[radix]进制,转换为10进制!!
console.log(paeseInt('10102px13',2)) //10
//找到符合二进制的 '1010'
//吧这个二进制的值转换为10进制 “按权展开”
//1*2^3+0*2^2+1*2^1+0*2^0 => 8+0+2+0 => 10
例题:
let arr = [27.2,0,'0013','14px',123];
arr = arr.map(parseInt);
console.log(arr); // [27,NaN,1,1,27]
解析:
arr.map((item,index) => { })
所以:
//等同于
parseInt(27.2,0) => parseInt('27.2',10) => 27
parseInt(0,1) => parseInt(0,1) => 不符合[2-36]区间,返回NaN
parseInt('0013',2) => parse('001',2) => 0*2^2+0*2^1+1*2^0 =>0+0+1 => 2
parseInt('14px',3) => 3进制转换为10进制 => parseInt('1',3) => 1*3^0 => 1
parseInt(123,4) => 4进制转换为10进制 => parseInt('123',4) => 1*4^2+2*4^1+3*4^0 => 16+8+3 => 27
JS中遇到以0开始的“数字”,会默认把其按照8进制转为10进制。然后再进行其他操作。
parseInt(0013,2) =>0*8^3+0*8^2+1*8^1+3*8^0=> 0+0+8+3=11 => parseInt('11',2) => 1*2^1+1*2^0 =>2+1=3
parseInt('0013',2) => 0*2^3+0*2^2+1*2^1+3*2^0 => 2+3 => 5
parseFloat([val]):
没有进制。
3、将其他类型转换为Boolean
除了 “0 / NaN / 空字符串 / null / undefined” 这五个值是false以外,其余的全是true
Boolean(0)
=> false
Boolean(NaN)
=> false
Boolean('')
=> false
Boolean(null)
=> false
Boolean(undefined)
=> false
Boolean([])
=> true
Boolean({})
=> true
Boolean(function(){})
=> true
4、"=="比较的相互转换规则
“==”相等,两边类型不同,需要先进行类型转换为相同类型,然后再进行比较:
-
@1 对象==字符串 对象转为字符串[ Symbol.toPrimitive -> valueOf -> toString ]
-
@2 null == undefined -> true ,null/undefined和其他任何值都不相等
null === undefined -> false
-
@3 对象==对象,比较的是堆内存地址,地址相同则相等
-
@4 NaN !== NaN
-
@5 除了以上的情况,只要两边类型不一致,剩下的都是先转换为数字,然后再进行比较的“===”绝对相等,如果两边类型不同,则直接是false,不会转换数据类型[推荐]
console.log(NaN == NaN) //false
console.log(Object.is(NaN,NaN)) //true
console.log([] == false); //都转为数字 0==0 => true
console.log(![] == false) //先处理 ![] => false => false == false => true
console.log([] == ![]) //true
解:
- ![]的优先级更高,先处理![],所以![] -> false
- 等价于了 [] == false
- 不属于以上4种情况,属于@5,两边类型不一致,先转为数字,在进行比较:
- []转为数字为0
- false转为数字为0
- 0 === 0
5、装箱和拆箱
装箱
let num = 10;
console.log(num.toFixed(2)) //10.00
// num是原始值,不是对象,按常理来说,是不能做“成员访问的”
// 默认装箱操作: new Number(num) 将其变为非标准的特殊对象,这样就可以调用 toFixed了
拆箱
let num = new Number(10);
console.log(num + 10); //20
在操作的过程中,浏览器会把num这个非标准的特殊对象变为原始值[Symbol.toPrimitive -> valueOf... ],这个操作就叫拆箱。
6、Js中有关(小数)浮点数的计算会出现精准度丢失的现象:
0.1 + 0.2 === 0.3 -> false
js的所有值在底层都是以二进制进行存储的(浮点苏转为二进制,可能出现无限循环的情况,计算机设计的问题,不是js语言的问题)
在计算机底层存储的时候,最多存储64位,那么就得舍弃一些值,值本身就失去了精准度。
处理:
(1) +(0.1+0.2).toFixed(2) :保存小数点后两位
(2) 扩大系数法
//获取需要扩大的系数
const coefficient = function(num) {
num = num+'';
let [,char=''] = num.split('.');
let len = char.length;
return Math.pow(10,len) //10**len
}
const plus = function(num1,num2){
num1 = +=num1;
num2 = +=num2;
if(isNaN(num1) || isNaN(num2)) return NaN;
let max = Math.max(coefficient(num1),coefficient(num2))
return (num1 * max + num2 * max) / max;
}
console.log(plus(0.1,0.2))
例2:
var a =?;
if(a ==1 && a ==2 && a == 3) {
console.log('OK')
}
a应该等于多少呢?
(1)利用 == 比较会转换数据类型,而对象转数字会经历3个步骤,我们重新将转换步骤中的某一个重写...
var a ={
i:0,
[Symbol.toprimitive]() {
// this -> a
return ++this.i
}
}
if(a ==1 && a ==2 && a == 3) {
console.log('OK')
}
(2)利用数组
var a = [1,2,3]
a.toString = a.shift;
if(a ==1 && a ==2 && a == 3) {
console.log('OK')
}
(3)利用数据劫持
//在全局上下文中,获取a的值:首先看VO(G)中有没有,没有再去Go(window)中查找
var i = 0;
Object.definePrototy(window,'a',{
get() {
return ++i;
}
})
if(a ==1 && a === 2 && a == 3) {
console.log('OK')
}
7、堆栈内存存储和运行代码
浏览器打开一个页面,首先会从计算机的虚拟内存(内存条)中分配两块内存出来
-
栈内存 stack 【ECStack】
- 共代码执行
- 存储声明的变量和原始值类型的值
-
堆内存 Heap
- 存储对象类型的值
默认在堆内存中,开辟一个空间【16进制地址】,GO(global object) 全局对象:存储了浏览器为js提供的内置API
紧接着,会创建一个全局的执行上下文 EC(G)
- 供全局代码执行的环境
- 进栈执行(可以吧执行上下文当做栈内存空间)
代码执行过程中,可能会声明变量,所以需要一个存放变量的对方 -> 变量对象 VO/AO
let a = 12;
@1 创建值
+ 原始值类型:直接存储在栈中 12
+ 对象类型:需要在堆内存中,重新开辟一块空间(16进制地址),把键值对存储到空间中,最后把地址赋值给变量。
@2 声明一个变量 a [放到变量对象中存储]
@3 吧创建的值赋值给声明的变量 [让变量和值关联在一起(建立指针指向)] “定义 defined”
let a = {n:1};
let b = a;
a.x = a = {n:2};
console.log(a.x);
console.log(b);
注意:使用var function 声明的变量,并没有放在VO(G)中
基于let/const 声明的变量,存储在VO(G)中的。
console.log(a) 先看VO(G),再看GO,如果GO中也没有,则报错 a is not defined
console.log(window.a) 直接去GO中找,如果没有不会报错,值是undefied
有关函数
函数是一种特殊的对象【可执行对象】:函数(普通函数、构造函数)、对象
创建函数:
-
开辟堆内存空间【16进制地址】
-
存储信息
-
创建函数的时候,就声明了其作用域,在哪个上下文中创建的,其作用域就是谁【宿主环境】
-
吧函数体中的代码当做字符串存储起来
-
当做普通对象,存储键值对
- name 函数名 length 形参格式 prototype 原型对象 [[prototype]]原型链 ...
-
-
把空间地址赋值给变量(函数名)
函数执行:
-
会创建一个“全新的” 、“私有”执行上下文 +EC(?) +AO(?) 私有变量对象 active活动的
-
代码执行之前,处理很多事情
- 初始化作用域链,有两端 <函数私有上下文,函数的作用域>
- 初始化THIS
- 初始化ARGUMENTS(实参集合)
- 形参赋值 [私有变量 -> AO]
- 变量提升
-
代码执行
-
关于上下文的回收释放问题
- 一般情况下,函数执行完,所形成的的私有上下文会被释放掉
作用域链:规划出变量查找的过程
- 私有上下文中遇到一个变量,首先看是否是自己私有的,如果是:接下来对变量的操作都是处理私有的,和外界毫无关系 【私有变量被“保护”起来了】 如果不是私有的,则基于作用域链,去上级上下文(函数的作用域)中找。
- 如果还找不到,则继续基于作用域链向上查找...
- 直到找到EC(G)为止。
let x = [12,13];
const fn = function(y) {
y[0] = 100;
y = [100];
y[1] = 200;
console.log(y); // [100,200]
}
fn(x);
console.log(x); // [100,23]
8、JS代码执行的预处理机制:变量提升(现在基本不咋用了)
/**
* EC(G)全局执行上下文
VO(G)/GO
a -> 12
变量提升: var a;
代码执行:
*/
//其实最开始浏览器从服务器获取到的js都是文本(字符串),只不过声明了其格式是[Content-Type:application/javascript;],浏览器首先按照这个格式去解析代码 -> ‘词法解析’阶段[目标是生成“AST词法解析树”]
//基于let/const等声明的变量:在词法解析阶段,其实已经明确了,未来在此上下文中,必然会存在某些变量,但是并没有声明;代码执行中,如果出现在具体声明的代码之前使用这个变量,浏览器会抛出错误!!
console.log(a); //undefined
var a = 12;
console.log(b); //报错:Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 12;
AST = {
EC(G):{
VO(G):{
},
...
}
}
console.log(fn) // function fn() {console.log(2)}
function fn() {console.log(1)}
console.log(fn) // function fn() {console.log(2)}
var fn = 12;
console.log(fn); // 12
function fn() {console.log(2)}
console.log(fn); // 12
/**
EC(G)
VO(G)/GO
fn -> 0x001 [[scope]]:EC(G)
-> 0x002 [[scope:EC(G)]]
-> 12
变量提升:
function fn() {console.log(1)}
var fn; //上下文中已经存在fn变量,不会重复声明
function fn() {console.log(2)}
代码执行:
*/
总结:function声明的优先级更高,var声明的优先级低于function声明的,所以会优先提升function
以上代码等同于:
function fn() {console.log(1)}
function fn() {console.log(2)}
//var fn; //此处由于上下文中已经存在fn变量,不会重复声明
console.log(fn) // function fn() {console.log(2)}
console.log(fn) // function fn() {console.log(2)}
fn = 12;
console.log(fn); // 12
console.log(fn); // 12
变量提升:不论条件是否成立,都要进行变量提升(对于var来讲新老版本浏览器没有任何影响,但是对于判断体中出现的function来讲,新老版本表现不一致:老版本 函数还是声明+定义 新版本 函数只会声明,不再定义)
console.log(a); // undefiend
if(!('a' in window)) {
var a = 13;
}
console.log(a) // undefiend
// 'a' in window -> true
// !('a' in window) -> false
// 等同于
var a;
console.log(a);
if(false) {
a = 13;
}
console.log(a);
上下文:全局上下文(EC(g))、函数上下文(EC(?))、块级上下文(EC(block))
块级上下文:除“函数和对象”的大括号以外,【例如:判断体、循环体、代码块...】,如果在大括号中出现了let const function class 等关键词声明变量,则当前大括号会产生一个“块级私有上下文”;它的上级上下文是所处的环境;var不产生,也不受块级上下文影响;
- 函数是个渣男
- 循环中的块级上下文
console.log(a) // undefined
// console.log(b) //报错:Uncaught ReferenceError: Cannot access 'b' before initialization
var a = 12;
let b = 13;
if(1 == 1) {
console.log(a) //12
//console.log(b) //报错:Uncaught ReferenceError: Cannot access 'b' before initialization
var a = 100;
let b = 200;
console.log(a) //100
console.log(b) //200
}
console.log(a) //100
console.log(b) //13
console.log(foo); //undefined
if(1 === 1) {
console.log(foo); //foo function(){}
function foo(){}
foo=1;
console.log(foo); //1
}
console.log(foo); //foo function() {}
---------------->>>>>>>>>>>>>>>
例二:
f = function() {return true};
g = function() {return false};
(function() {
if(g() && [] == ![]){ // 报错:g is not a function
f= function() {return false}
function g() {return true}
}
})();
console.log(f()); //
console.log(g()); //
@1 在立即执行函数上下文中,function声明的g会发生变量提升,但是在ES6的新语法中,块级作用域中function声明的变量提升和在全局作用域中function声明的变量提升是不一样的。
-
在全局作用域和函数作用域中,function声明的变量提升会直接声明并赋值
-
在块级作用域中
- 会先在块级作用域的上一层(也就是作用域链的上一层)发生变量提升,但是在这里只是声明了变量,不会赋值
- 然后在块级作用域中会形成块级上下文,在这里也会发生变量提升,并且 会声明并赋值
- 当在块级作用域中执行到声明变量那里时,会在上一层(第一步那里)的作用域中的声明的变量赋值
f = function() {return true};
g = function() {return false};
(function() {
console.log(g()) //true
function g() {
return true
}
})();
f = function() {return true};
g = function() {return false};
(function() {
console.log(g) //undefined
if(true){
function g() {
return true
}
}
console.log(g()) //true
})();
未完待续~