💻 前端高频面试题—JavaScript篇(一) 🏠专栏:前端面试题
👀个人主页:繁星学编程🍁
🧑个人简介:一个不断提高自我的平凡人🚀
🔊分享方向:目前主攻前端,其他知识也会阶段性分享🍀
👊格言:☀️没有走不通的路,只有不敢走的人!☀️
👉让我们一起进步,一起成为更好的自己!!!🎁
文章目录
前端高频面试题—JavaScript篇(一)
本文主要讲解的前端高频面试题知识有:
- 讲讲JS的数据类型
- 检测数据类型
- 作用域
- 作用域链
- 预解析
一. 讲讲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需要注意:
- symbol函数前不能使用new关键字,否则会报错,这是因为symbol是原始数据类型,而不是对象,所以不能添加属性
- symbol可以接受一个字符串作为参数,表示对Symbol的描述,主要是在控制台显示时容易区分。这个参数可以不加,如果不加在控制台输出就是两个Symbol()不利于区分,加上参数就是为了加以区分。
- Symbol是唯一的与谁都不相等
- Symbol不能与其他值进行运算,否则会报错
- Symbol 可以显示的转为字符串,布尔值,但是不能转为数字,转为数字会报错
- 由于每一个Symbol都不相同,那么可以作为标识符作为对象的属性名,保证不会出现同名的的属性
- 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); // bigintconsole.log(100n + 100n); // 200n
console.log(100n + 100); // Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
注:
-
将任意数字转为BigInt , 在数字后面添加n
-
bigInt只能和bigInt一起操作
-
BigInt
和Number
不是严格相等的,但是宽松相等的。10n == 10; // true 10n === 10; // false
二. 检测数据类型
判断数据类型的方式
-
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
-
constructor
语法:数据结构.constructor
返回值:该数据结构所属的构造函数
缺点:无法检测 undefined 和 nullconsole.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')
-
instanceof
语法:数据结构 instanceof 构造函数
返回值:true/false
缺点:无法检测 undefined 和 nullconsole.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
-
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
无法检测 undefined 和 nullinstanceof
无法检测 undefined 和 nullObject.prototype.toString.call()
所有数据类型都可以检测
三. 作用域
JavaScript中的作用域主要有:全局作用域、函数(局部)作用域、块级作用域
什么是作用域?
就是一个变量可以生效(访问)的范围,变量不是在所有地方都可以使用的,而这个变量的使用范围就是作用域。
目的
:为了提高程序的可靠性和减少命名冲突
(1) 全局作用域
直接在script标签中编写的代码都运行在全局作用域中。
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。
<script>let a = 10;console.log(a);// 10function fn() {console.log(a); // 10a = a + 10;console.log(a); // 20return a;}fn()console.log(a); // 20
</script>
注意事项:
- 全局作用域在打开页面时创建,在页面关闭时销毁。
- 全局作用域中有一个全局对象window,window对象由浏览器提供,可以在页面中直接使用,它代表的是整个的浏览器的窗口。
- 在全局作用域中创建的变量都会作为window对象的属性保存,在全局作用域中创建的函数都会作为window对象的方法保存。
- 在全局作用域中创建的变量和函数可以在页面的任意位置访问。在函数作用域中也可以访问到全局作用域的变量。
(2) 局部作用域
**局部作用域(也叫函数作用域)**就是在全局作用域下面有开辟出来的一个相对小一些的作用域,在局部作用域中定义的变量只能在这个局部作用域内部使用。
在JavaScript中只有函数能生成一个局部作用域, 别的都不行。
var a = 10;
function fn(){var a = 20;console.log(a);
}
fn();// 20
注意事项:
-
函数作用域是函数执行时创建的作用域,每次调用函数都会创建一个新的函数作用域。
-
函数作用域在函数执行时创建,在函数执行结束时销毁。
-
在函数作用域中创建的变量,不能在全局中访问。
-
当在函数作用域中使用一个变量时,它会先在自身作用域中寻找:
如果找到了则直接使用,如果没有找到则到上一级作用域中寻找;
如果找到了则使用,找不到则一直会继续向上找。
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引入了let
和const
关键字,和var
关键字不同,在大括号中使用let
和const
声明的变量存在于块级作用域中。在大括号之外不能访问这些变量。
if (10 > 1) {let num = 100;console.log(num); // 100const 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 === 100n = 500; // 赋值 ,在自己作用域中没找到,向上一级找,找到window 中有一个n n === 500console.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);
解题思路:
-
打开浏览器
-
预解析
// 只有第一行代码需要预解析 // 提前声明一个变量a 不赋值
-
代码执行
// 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
解题思路:
-
打开浏览器
-
进行预解析
// 只有第一行代码需要预解析 // 提前声明一个变量a 不赋值
-
代码执行
// 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();
针对以上的两个问题,就要说到预解析的作用了。
预解析特点:
- 预解析只会解析 var 声明的变量 和 声明式函数
- 赋值式函数,按照var 的方式进行预解析
- 函数里面的变量,不会进行预解析,
- 函数里面的代码会在该函数调用的时候,进行预解析
预解析:解析 var 声明的变量
提前声明这一个变量,但是不赋值
预解析:解析 声明式函数
提前声明这个变量,并且赋值为一个函数
二. 解读预解析两步
预解析的两大步骤
(1) 预编译,代码进入浏览器逐行执行之前干的事情(不可见)(案例1-4会分别解释以下四句话)
- 先找var和function关键字,找到var,提前赋值undefined。找到function,提前将整个函数体赋值给函数名称。
- 如果预编译函数名和变量名出现重复,函数名优先(去除变量名)
- 函数内部依然做预编译,同时函数的参数类似函数内部的变量,也要做预编译。
- 函数如果带有形参,要先形参赋值,在进行预解析
(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); // 输出:undefinedvar 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 = 20return a;
}
console.log(fn(a)); // 输出:20
结束语:
希望对您有一点点帮助,如有错误欢迎小伙伴指正。
👍点赞:您的赞赏是我前进的动力!
⭐收藏:您的支持我是创作的源泉!
✍评论:您的建议是我改进的良药!
一起加油!!!💪💪💪