前端高频面试题—JavaScript篇(一)

💻 前端高频面试题—JavaScript篇(一) 🏠专栏:前端面试题
👀个人主页:繁星学编程🍁
🧑个人简介:一个不断提高自我的平凡人🚀
🔊分享方向:目前主攻前端,其他知识也会阶段性分享🍀
👊格言:☀️没有走不通的路,只有不敢走的人!☀️
👉让我们一起进步,一起成为更好的自己!!!🎁

前端高频面试题—JavaScript篇(一)

本文主要讲解的前端高频面试题知识有:

  1. 讲讲JS的数据类型
  2. 检测数据类型
  3. 作用域
  4. 作用域链
  5. 预解析

一. 讲讲JS的数据类型

1. 基本数据类型(值类型)

基本类型:字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol。

2. 引用数据类型

引用数据类型:对象(Object)、数组(Array)、函数(Function)。(数组和函数都是属于对象的数据类型)

3. NaN是什么类型?

NaN是一个数值类型(number),但不是一个具体数字

console.log(typeof NaN) // number

4. null和undefined区别

null会被隐式转化为0,不容易发现错误,先有null后有undefined,undefined就是为了填补之前的坑(null表示一个空对象指针,转化为数值为0;undefined转化为数值是NaN)

5. Symbol用法

注:Symbol 是 ES6 引入了一种新的原始数据类型,表示独一无二的值。

Symbol 本质上是一种唯一标识符,可用作对象的唯一属性名,这样其他人就不会改写或覆盖你设置的属性值。

const a = Symbol();
console.log(a); // Symbol()
const b = Symbol("test");
console.log(b); // Symbol('test');
const c = Symbol();
console.log(c == a); // false
console.log(c.toString()); // 'Symbol()'
var mySymbol = Symbol();
//第一种写法
var a1 = {};
a1[mySymbol] = "Hello!";
console.log(a1[mySymbol]); // Hello!
//第二种写法
var a2 = {
    [mySymbol]: "Hellow!",
};
console.log(a2[mySymbol]); // Hellow!
//第三种写法
var a3 = {};
Object.defineProperty(a3, mySymbol, { value: "Hellow!" });
console.log(a3[mySymbol]); // Hellow!

Symbol需要注意:

  1. symbol函数前不能使用new关键字,否则会报错,这是因为symbol是原始数据类型,而不是对象,所以不能添加属性
  2. symbol可以接受一个字符串作为参数,表示对Symbol的描述,主要是在控制台显示时容易区分。这个参数可以不加,如果不加在控制台输出就是两个Symbol()不利于区分,加上参数就是为了加以区分。
  3. Symbol是唯一的与谁都不相等
  4. Symbol不能与其他值进行运算,否则会报错
  5. Symbol 可以显示的转为字符串,布尔值,但是不能转为数字,转为数字会报错
  6. 由于每一个Symbol都不相同,那么可以作为标识符作为对象的属性名,保证不会出现同名的的属性
  7. Symbol值作为对象的属性名时不能使用点运算符,同理,在对象的内部使用Symbol值时也必须放在方括号中

6. BigInt

BigInt 是一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数。这原本是 Javascript 中可以用Number 表示的最大数字。BigInt 可以表示任意大的整数。

新增加这个数据类型的目的:js存储16位以上的数据会出现精度丢失的问题

var num = 100;
num = 100n; // 转为BigInt
console.log(num); // 100n
console.log(typeof num); // bigint

console.log(100n + 100n); // 200n
console.log(100n + 100); // Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

  • 将任意数字转为BigInt , 在数字后面添加n

  • bigInt只能和bigInt一起操作

  • BigIntNumber不是严格相等的,但是宽松相等的。

    10n == 10; // true
    10n === 10; // false
    

二. 检测数据类型

判断数据类型的方式

  1. typeof

    语法:typeof(要测试的数据)
    返回值:测试数据对应的数据类型
    缺点:只能检测基本数据类型

    console.log(typeof (12)); // number
    console.log(typeof ('null')); // string
    console.log(typeof (null)); // object
    console.log(typeof (undefined)); // undefined
    console.log(typeof (true)); // boolean
    console.log(typeof ([])); // object
    console.log(typeof ({})); // object
    

    :要注意null的数据类型是Object

  2. constructor

    语法:数据结构.constructor
    返回值:该数据结构所属的构造函数
    缺点:无法检测 undefined 和 null

    console.log([].constructor); // ƒ Array() { [native code] }
    console.log({}.constructor); // ƒ Object() { [native code] }
    console.log(function () { }.constructor); // ƒ Function() { [native code] }
    console.log((1).constructor); // ƒ Number() { [native code] }
    console.log(('zs').constructor); // ƒ String() { [native code] }
    console.log((null).constructor); // Uncaught TypeError: Cannot read properties of null (reading 'constructor')
    console.log((undefined).constructor); // Uncaught TypeError: Cannot read properties of undefined (reading 'constructor')
    
  3. instanceof

    语法:数据结构 instanceof 构造函数
    返回值:true/false
    缺点:无法检测 undefined 和 null

    console.log([] instanceof Array); // true
    console.log([] instanceof Object); // true
    console.log([] instanceof String); // false
    console.log(null instanceof Object); // false
    console.log(undefined instanceof Object); // false
    let a = 1;
    console.log(a instanceof Number); // false
    
  4. Object.prototype.toString.call()

    语法:Object.prototype.toString.call(要测试的数据)
    返回值:‘[Object 数据类型]’
    所有数据类型都可以检测

    console.log(Object.prototype.toString.call(1)); // [object Number]
    console.log(Object.prototype.toString.call([])); // [object Array]
    console.log(Object.prototype.toString.call({})); // [object Object]
    console.log(Object.prototype.toString.call(true)); // [object Boolean]
    
    console.log(Object.prototype.toString.call(123)); // [object Number]
    console.log(Object.prototype.toString.apply(123)); // [object Number]
    console.log(Object.prototype.toString.bind(123)()); // [object Number]
    

    :不必一定用call()也可以使用bind()()apply(),只要是立即执行即可。

总结区别

  • typeof只能检测基本数据类型
  • constructor 无法检测 undefinednull
  • instanceof 无法检测 undefinednull
  • Object.prototype.toString.call() 所有数据类型都可以检测

三. 作用域

JavaScript中的作用域主要有:全局作用域、函数(局部)作用域、块级作用域

什么是作用域?

就是一个变量可以生效(访问)的范围,变量不是在所有地方都可以使用的,而这个变量的使用范围就是作用域。

目的:为了提高程序的可靠性和减少命名冲突

(1) 全局作用域

直接在script标签中编写的代码都运行在全局作用域中。

任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。

<script>
    let a = 10;
    console.log(a);// 10
    function fn() {
        console.log(a); // 10
        a = a + 10;
        console.log(a); // 20
        return a;
    }
    fn()
    console.log(a); // 20
</script>

注意事项

  1. 全局作用域在打开页面时创建,在页面关闭时销毁。
  2. 全局作用域中有一个全局对象window,window对象由浏览器提供,可以在页面中直接使用,它代表的是整个的浏览器的窗口。
  3. 在全局作用域中创建的变量都会作为window对象的属性保存,在全局作用域中创建的函数都会作为window对象的方法保存。
  4. 在全局作用域中创建的变量和函数可以在页面的任意位置访问。在函数作用域中也可以访问到全局作用域的变量。

(2) 局部作用域

**局部作用域(也叫函数作用域)**就是在全局作用域下面有开辟出来的一个相对小一些的作用域,在局部作用域中定义的变量只能在这个局部作用域内部使用。

在JavaScript中只有函数能生成一个局部作用域, 别的都不行。

var a = 10;
function fn(){
	var a = 20;
	console.log(a);
}
fn();// 20

注意事项

  1. 函数作用域是函数执行时创建的作用域,每次调用函数都会创建一个新的函数作用域。

  2. 函数作用域在函数执行时创建,在函数执行结束时销毁。

  3. 在函数作用域中创建的变量,不能在全局中访问。

  4. 当在函数作用域中使用一个变量时,它会先在自身作用域中寻找:

    如果找到了则直接使用,如果没有找到则到上一级作用域中寻找;

    如果找到了则使用,找不到则一直会继续向上找。

var a = 10; // 全局作用域
function fn() {
    var b = 100; // 局部作用域  
    console.log(a); // 10  a是全局
    console.log(b); // 100 b是局部,函数内部使用
}
fn();
console.log(a); // 10  a是全局
console.log(b); // 报错,b是局部,函数外面无法访问

(3) 块级作用域

ES6引入了letconst关键字,和var关键字不同,在大括号中使用letconst声明的变量存在于块级作用域中。在大括号之外不能访问这些变量。

if (10 > 1) {
    let num = 100;
    console.log(num); // 100
    const a = 200; 
    console.log(a); // 200
}
console.log(a); // 报错:Uncaught ReferenceError: a is not defined
console.log(num);

特殊情况

如果函数内部声明的变量,声明的时候没有添加var关键字,函数内部声明的变量视为全局变量。

(1) 外部定义了全局变量,函数内部直接改变这个全局变量

var a = 10; //全局
function fn() {
    a = 100; //可以使用全局,改变全局
    console.log(a); //100  全局
}
fn();
console.log(a); //100  全局

(2) 外部没有定义了全局变量,而是忽略的var关键字,b变成全局变量

function fn1() {
    b = 200; 
    console.log(b); //200
}
fn1();
console.log(b); //200

四. 作用域链

(1) 概念

内部函数访问外部函数的变量,采用的是链式查找的方式来决定取哪个值。

var a = 10;
function fn() {
    var a = 20;
    function fn2() {
        console.log(a);
    }
    fn2();
}
fn();// 20

有了作用域以后,变量就有了使用范围,也就有了使用规则

变量使用规则分为两种:访问规则赋值规则

(2) 访问规则

// 当我想获取一个变量的值的时候,我们管这个行为叫做访问
var a = 10;
console.log(a);//访问

首先,在自己的作用域内部查找

  • 如果有,就直接拿来使用;
  • 如果没有,就去上一级作用域查找,如果有,就拿来使用;
  • 如果没有,就继续去上一级作用域查找,依次类推;
  • 如果一直到全局作用域都没有这个变量,那么就会直接报错(该变量 is not defined)
var a = 10; // 全局作用域 全局变量
function fn1() {
    var a = 100; // 局部变量 f1作用域中的局部变量
    function fn2() {
        // var a = 1000; // 局部变量  f2作用域中的局部变量
        console.log(a); 
    }
    fn1();
}
fn(); // 100

(3) 赋值规则

当你想给一个变量赋值的时候,那么就先要找到这个变量,再给他赋值。

  • 先在自己作用域内部查找,有就直接赋值;
  • 没有就去上一级作用域内部查找,有就直接赋值;
  • 还没有再去上一级作用域查找,有就直接赋值;
  • 如果一直找到全局作用域都没有,那么就把这个变量定义为全局变量,再给他赋值。
function fn() {
    var a = b = 3; // b = 3   var a = b;
    console.log(a, b); // 3,3
}
fn();
console.log(b); // 3 // 注:b的声明前面没有var关键字,变成全局的
console.log(a); // 报错,a是局部变量

(4) 案例

案例1

先在自己作用域内部查找,有就直接赋值;

var n = 100;
function f1() {
    var n = 200;
    console.log("f1 前", n); // 找自己作用域中的变量n, 如果有,直接使用
    n = 500; // 赋值,先找自己作用域中的变量n,如果有,直接赋值,这里把自己作用域中的n修改掉
    console.log("f1 后", n); // 访问,找自己作用域中的变量n,如果有,直接使用
}
console.log("全局前", n); // 访问规则,找全局变量n n = 100
f1();
console.log("全局后", n); // 访问机制,找全局变量n n = 100

案例2

没有就去上一级作用域内部查找,有就直接赋值;
还没有再去上一级作用域查找,有就直接赋值;

var n = 100;
function f1() {
    console.log("f1 前", n); // 第二个打印,在自己作用域中找,向上一级找,找到window 中有一个n n === 100
    n = 500; // 赋值 ,在自己作用域中没找到,向上一级找,找到window 中有一个n n === 500
    console.log("f1 后", n); // 第三个打印,在自己作用域中找没找到,向上一级找,找到window中有一个n  n === 500
}
console.log("全局前", n); // 第一个打印,自己作用域(window) 找到全局变量n n = 100
f1(); // 第二步 函数调用,找到函数体里面的代码执行
console.log("全局后", n); // 第四个打印,找自己作用域(window) 找到了 n === 500

案例3

如果一直找到全局作用域都没有,那么就把这个变量定义为全局变量,再给他赋值。

function f1() {
    n = 500; // 赋值  从自己作用域中找,没找到,向上找,找到window ,window没有,直接把这个n当做全局变量n进行赋值
    console.log("f1后", n); // 从自己找,没找到向上找,找到window window有一个全局变量n n === 500
}
f1();
console.log("全局", n); // 从自己找,没找到向上找,找到window,window有一个全局变量n n === 500

相关面试题

面试题一
var a = b;
a = 20;
b = 20;
console.log(a); // 报错,b is not defined
console.log(b); 

解题思路

  1. 打开浏览器

  2. 预解析

    // 只有第一行代码需要预解析
    // 提前声明一个变量a 不赋值
    
  3. 代码执行

    // a = b 赋值
    // 把b变量对应的值赋给a
    // 有没有b这个变量, 在作用域中找到b这个变量,报错 b is not defined 
    
面试题二
var a = b = 10
a = 20
b = 20
console.log(a) // 20
console.log(b) // 20

解题思路

  1. 打开浏览器

  2. 进行预解析

    // 只有第一行代码需要预解析
    // 提前声明一个变量a 不赋值
    
  3. 代码执行

    // 1.执行第一行代码
    	// var a = b = 10 实际上等同于 var a = (b = 10) 
    	// b = 10要先处理,按照赋值机制处理,这里把b定义为全局变量再进行赋值
    	// 然后再将b中得到的10赋值给a
    // 2.
    	// a = 20 给a重新赋值为20
    	// b = 20 给b重新赋值为20
    // 3. 
    	// console.log(a) 打印a的值 // 20
    	// console.log(b) 打印b的值 // 20
    

五. 预解析

一.概述

在代码执行之前,对代码进行通读并解析

首先,思考两个问题

为什么变量可以变量提升,并且同时输出undefined?

console.log(a); // undefined
var a = 1;
console.log(a); // 1

为什么声明式函数可以在任意地方进行调用?

fn();
function(){
	console.log(1);
}
fn();

针对以上的两个问题,就要说到预解析的作用了。

预解析特点

  1. 预解析只会解析 var 声明的变量声明式函数
  2. 赋值式函数,按照var 的方式进行预解析
  3. 函数里面的变量,不会进行预解析,
  4. 函数里面的代码会在该函数调用的时候,进行预解析

预解析:解析 var 声明的变量

提前声明这一个变量,但是不赋值

预解析:解析 声明式函数

提前声明这个变量,并且赋值为一个函数

二. 解读预解析两步

预解析的两大步骤

(1) 预编译,代码进入浏览器逐行执行之前干的事情(不可见)(案例1-4会分别解释以下四句话)

  1. 先找var和function关键字,找到var,提前赋值undefined找到function,提前将整个函数体赋值给函数名称
  2. 如果预编译函数名和变量名出现重复,函数名优先(去除变量名)
  3. 函数内部依然做预编译,同时函数的参数类似函数内部的变量,也要做预编译。
  4. 函数如果带有形参,要先形参赋值,在进行预解析

(2) 逐行执行,代码进入浏览器,可以根据浏览器返回的信息,查看结果,同时遇到代码错误,立刻停止执行。

函数声明直接跳过,函数必须调用才有意义。

案例1

分析预编译的结果:提前进入浏览器的引擎
先找var和function关键字,找到var,提前赋值undefined找到function,提前将整个函数体赋值给函数名称

// 要执行的代码
console.log(a);
fn();
var a = 10; 
console.log(a); 
function fn() {
    console.log('函数');
}
fn();

分析思路

// 1.首先对上述代码进行预编译:
a = undefined;
fn = function fn() {console.log('函数');}

// 2.然后,进入逐行执行:
console.log(a); // 输出:undefined
fn(); // 输出:函数
var a = 10; // 这里将预编译的结果进行覆盖:a = 10
console.log(a); // 输出:10
function fn() {
    console.log('函数');
}
fn(); // 输出:函数
案例2

分析预编译的结果:提前进入浏览器的引擎
如果预编译函数名和变量名出现重复,函数名优先(去除变量名)

// 要执行的代码
console.log(a); 
var a = 10; 
console.log(a); 
function a() {
    console.log('函数');
}
console.log(a); 

分析思路

// 1.首先,对上述代码进行预编译:
// 由于函数名和变量名出现重复,函数名优先(去除变量名)
a =  function a() {console.log('函数');}

// 2.然后,进入逐行执行:
console.log(a); // 输出:function a() {console.log('函数');}
var a = 10; // 修改预编译的结果: a = 10
console.log(a); // 输出:10
function a() {
    console.log('函数');
}
console.log(a); // 输出:10
案例3

分析函数内部预编译的结果:提前进入浏览器的引擎
函数内部依然做预编译,同时函数的参数类似函数内部的变量,也要做预编译。

注意:只要函数存在形参,一定要做预编译。

// 要执行的代码
var a = 1;
function fn() {
    console.log(a); 
    var a = 20;
    console.log(a); 
}
console.log(fn());

分析思路

// 1.首先进行预编译:
a = undefined;
fn = function fn(a) { console.log(a); a = 20; console.log(a); }
// 注意:只要函数存在形参,一定要做预编译。
// 2.代码依次执行
var a = 1;
function fn() { 
    // 2.1首先函数内部预解析
    a = undefined;
    // 2.2然后执行代码
    console.log(a); // 输出:undefined
    var a = 20;
    console.log(a); // 修改预编译的结果: a = 20
}
console.log(fn()); // 输出:undefined(因为函数没有写return,没有返回值)
案例4

分析函数内部预编译的结果:提前进入浏览器的引擎
函数内部要做预编译,同时函数如果带有形参,要先形参赋值,在进行预解析。

// 要执行的代码
var a = 1;
function fn(a) {
    console.log(a); 
    var a = 20;
    console.log(a); 
}
console.log(fn(a));

分析思路

// 1.首先进行预编译:
a = undefined;
fn = function fn(a) { console.log(a); a = 20; console.log(a); }
// 注意:只要函数存在形参,一定要做预编译。
// 2.代码依次执行
var a = 1;
function fn(a) { 
    // 2.1首先函数内部预解析
    a = undefined;
    // 2.2然后执行代码
    console.log(a); // 输出:1(因为在预解析前先进行了形参赋值)
    var a = 20;
    console.log(a); // 修改预编译的结果: a = 20
    return a;
}
console.log(fn(a)); // 输出:20

结束语

希望对您有一点点帮助,如有错误欢迎小伙伴指正。
👍点赞:您的赞赏是我前进的动力!
⭐收藏:您的支持我是创作的源泉!
✍评论:您的建议是我改进的良药!
一起加油!!!💪💪💪

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

繁星学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值