前端面试题(持续更新~)

181 阅读14分钟

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:

image.png

再看看对象的:

image.png

明白了吧!

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);

image.png

注意:使用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]

image.png

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):{
            
        },
         ...
	}
}

image.png

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

image.png

image.png

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() {}

image.png ---------------->>>>>>>>>>>>>>>

image.png

例二:

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   
})();

未完待续~