前言
本文讲解 56 道 JavaScript 和 ES6+ 面试题的内容。
复习前端面试的知识,是为了巩固前端的基础知识,最重要的还是平时的积累!
注意
:文章的题与题之间用下划线分隔开,答案仅供参考。
前端硬核面试专题的完整版在此:前端硬核面试专题,包含:HTML + CSS + JS + ES6 + Webpack + Vue + React + Node + HTTPS + 数据结构与算法 + Git 。
JavaScript
常见的浏览器内核有哪些 ?
- Trident 内核:IE, 360,搜狗浏览器 MaxThon、TT、The World,等。[又称 MSHTML]
- Gecko 内核:火狐,FF,MozillaSuite / SeaMonkey 等
- Presto 内核:Opera7 及以上。[Opera 内核原为:Presto,现为:Blink]
- Webkit 内核:Safari,Chrome 等。 [ Chrome 的:Blink(WebKit 的分支)]
mouseenter 和 mouseover 的区别
- 不论鼠标指针穿过被选元素或其子元素,都会触发 mouseover 事件,对应 mouseout。
- 只有在鼠标指针穿过被选元素时,才会触发 mouseenter 事件,对应 mouseleave。
用正则表达式匹配字符串,以字母开头,后面是数字、字符串或者下划线,长度为 9 - 20
var re=new RegExp("^[a-zA-Z][a-zA-Z0-9_]{9,20}$");
手机号码校验
function checkPhone(){ var phone = document.getElementById('phone').value; if(!(/^1(3|4|5|7|8)\d{9}$/.test(phone))){ alert("手机号码有误,请重填"); return false; } } ^1(3|4|5|7|8)d{9}$,表示以 1 开头,第二位可能是 3/4/5/7/8 等的任意一个,在加上后面的 d 表示数字 [0-9] 的 9 位,总共加起来 11 位结束。 手机号码格式验证方法(正则表达式验证)支持最新电信 199, 移动 198, 联通 166 // 手机号码校验规则 let valid_rule = /^(13[0-9]|14[5-9]|15[012356789]|166|17[0-8]|18[0-9]|19[8-9])[0-9]{8}$/; if ( ! valid_rule.test(phone_number)) { alert('手机号码格式有误'); return false; }
这样 phone_number 就是取到的手机号码,即可!
js 字符串两边截取空白的 trim 的原型方法的实现
js 中本身是没有 trim 函数的。
// 删除左右两端的空格 function trim(str){ return str.replace(/(^\s*)|(\s*$)/g, ""); } // 删除左边的空格 /(^\s*)/g // 删除右边的空格 /(\s*$)/g
介绍一下你对浏览器内核的理解 ?
内核主要分成两部分:渲染引擎(layout engineer 或 Rendering Engine) 和 JS 引擎。
渲染引擎
负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入 CSS 等),以及计算网页的显示方式,然后会输出至显示器或打印机。
浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。
所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核。
JS 引擎
解析和执行 javascript 来实现网页的动态效果。
最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎。
哪些常见操作会造成内存泄漏 ?
内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。
垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收。
- setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏。
- 闭包、控制台日志、循环(在两个对象彼此引用且彼此保留时,就会产生一个循环)。
线程与进程的区别 ?
- 一个程序至少有一个进程,一个进程至少有一个线程。
- 线程的划分尺度小于进程,使得多线程程序的并发性高。
- 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。
- 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。
但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
eval() 函数有什么用 ?
eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。
实现一个方法,使得:add(2, 5) 和 add(2)(5) 的结果都为 7
var add = function (x, r) { if (arguments.length == 1) { return function (y) { return x + y; }; } else { return x + r; } }; console.log(add(2)(5)); // 7 console.log(add(2,5)); // 7
alert(1 && 2) 和 alert(1 || 0) 的结果是 ?
alert(1 &&2 ) 的结果是 2
- 只要 “&&” 前面是 false,无论 “&&” 后面是 true 还是 false,结果都将返 “&&” 前面的值;
- 只要 “&&” 前面是 true,无论 “&&” 后面是 true 还是 false,结果都将返 “&&” 后面的值;
alert(0 || 1) 的结果是 1
- 只要 “||” 前面为 false,不管 “||” 后面是 true 还是 false,都返回 “||” 后面的值。
- 只要 “||” 前面为 true,不管 “||” 后面是 true 还是 false,都返回 “||” 前面的值。
只要记住 0 与 任何数都是 0,其他反推。
下面的输出结果是 ?
var out = 25, inner = { out: 20, func: function () { var out = 30; return this.out; } }; console.log((inner.func, inner.func)()); console.log(inner.func()); console.log((inner.func)()); console.log((inner.func = inner.func)());
结果:25,20,20,25
代码解析:这道题的考点分两个
- 作用域
- 运算符(赋值预算,逗号运算)
先看第一个输出:25,因为 ( inner.func, inner.func ) 是进行逗号运算符,逗号运算符就是运算前面的 ”,“ 返回最后一个,举个栗子
var i = 0, j = 1, k = 2; console.log((i++, j++, k)) // 返回的是 k 的值 2 ,如果写成 k++ 的话 这里返回的就是 3 console.log(i); // 1 console.log(j); // 2 console.log(k); // 2
回到原题 ( inner.func, inner.func ) 就是返回 inner.func ,而 inner.func 只是一个匿名函数
function () { var out = 30; return this.out; }
而且这个匿名函数是属于 window 的,则变成了
(function () { var out = 30; return this.out; })()
此刻的 this => window
所以 out 是 25。
第二和第三个 console.log 的作用域都是 inner,也就是他们执行的其实是 inner.func();
inner 作用域中是有 out 变量的,所以结果是 20。
第四个 console.log 考查的是一个等号运算 inner.func = inner.func ,其实返回的是运算的结果,
举个栗子
var a = 2, b = 3; console.log(a = b) // 输出的是 3
所以 inner.func = inner.func 返回的也是一个匿名函数
function () { var out = 30; return this.out; }
此刻,道理就和第一个 console.log 一样了,输出的结果是 25。
下面程序输出的结果是 ?
if (!("a" in window)) { var a = 1; } alert(a);
代码解析:如果 window 不包含属性 a,就声明一个变量 a,然后赋值为 1。
你可能认为 alert 出来的结果是 1,然后实际结果是 “undefined”。
要了解为什么,需要知道 JavaScript 里的 3 个概念。
首先,所有的全局变量都是 window 的属性,语句 var a = 1; 等价于 window.a = 1;
你可以用如下方式来检测全局变量是否声明:"变量名称" in window。
第二,所有的变量声明都在范围作用域的顶部,看一下相似的例子:
alert("b" in window); var b;
此时,尽管声明是在 alert 之后,alert 弹出的依然是 true,这是因为 JavaScript 引擎首先会扫描所有的变量声明,然后将这些变量声明移动到顶部,最终的代码效果是这样的:
var a; alert("a" in window);
这样看起来就很容易解释为什么 alert 结果是 true 了。
第三,你需要理解该题目的意思是,变量声明被提前了,但变量赋值没有,因为这行代码包括了变量声明和变量赋值。
你可以将语句拆分为如下代码:
var a; //声明 a = 1; //初始化赋值
当变量声明和赋值在一起用的时候,JavaScript 引擎会自动将它分为两部以便将变量声明提前,
不将赋值的步骤提前,是因为他有可能影响代码执行出不可预期的结果。
所以,知道了这些概念以后,重新回头看一下题目的代码,其实就等价于:
var a; if (!("a" in window)) { a = 1; } alert(a);
这样,题目的意思就非常清楚了:首先声明 a,然后判断 a 是否在存在,如果不存在就赋值为1,很明显 a 永远在 window 里存在,这个赋值语句永远不会执行,所以结果是 undefined。
提前这个词语显得有点迷惑了,你可以理解为:预编译。
下面程序输出的结果是 ?
var a = 1; var b = function a(x) { x && a(--x); }; alert(a);
这个题目看起来比实际复杂,alert 的结果是 1。
这里依然有 3 个重要的概念需要我们知道。
- 首先,第一个是
变量声明在进入执行上下文就完成了
; - 第二个概念就是
函数声明也是提前的,所有的函数声明都在执行代码之前都已经完成了声明,和变量声明一样
。
澄清一下,函数声明是如下这样的代码:
function functionName(arg1, arg2){ //函数体 } 如下不是函数,而是函数表达式,相当于变量赋值: var functionName = function(arg1, arg2){ //函数体 };
澄清一下,函数表达式没有提前,就相当于平时的变量赋值。
- 第三需要知道的是,
函数声明会覆盖变量声明,但不会覆盖变量赋值
。
为了解释这个,我们来看一个例子:
function value(){ return 1; } var value; alert(typeof value); //"function"
尽管变量声明在下面定义,但是变量 value 依然是 function,也就是说这种情况下,函数声明的优先级高于变量声明的优先级,但如果该变量 value 赋值了,那结果就完全不一样了:
function value(){ return 1; } var value = 1; alert(typeof value); //"number"
该 value 赋值以后,变量赋值初始化就覆盖了函数声明。
重新回到题目,这个函数其实是一个有名函数表达式,函数表达式不像函数声明一样可以覆盖变量声明,但你可以注意到,变量 b 是包含了该函数表达式,而该函数表达式的名字是 a。不同的浏览器对 a 这个名词处理有点不一样,在 IE 里,会将 a 认为函数声明,所以它被变量初始化覆盖了,就是说如果调用 a(–x) 的话就会出错,而其它浏览器在允许在函数内部调用 a(–x),因为这时候 a 在函数外面依然是数字。
基本上,IE 里调用 b(2) 的时候会出错,但其它浏览器则返回 undefined。
理解上述内容之后,该题目换成一个更准确和更容易理解的代码应该像这样:
var a = 1, b = function(x) { x && b(--x); }; alert(a);
这样的话,就很清晰地知道为什么 alert 的总是 1 了。
下面程序输出的结果是 ?
function a(x) { return x * 2; } var a; alert(a); alert 的值是下面的函数 function a(x) { return x * 2; }
这个题目比较简单:即函数声明和变量声明的关系和影响,遇到同名的函数声明,不会重新定义。
下面程序输出的结果是 ?
function b(x, y, a) { arguments[2] = 10; alert(a); } b(1, 2, 3);
结果为 10。
活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。
三道判断输出的题都是经典的题
var a = 4; function b() { a = 3; console.log(a); function a(){}; } b();
明显输出是 3,因为里面修改了 a 这个全局变量,那个 function a(){} 是用来干扰的,虽然函数声明会提升,就被 a 给覆盖掉了,这是我的理解。
不记得具体的,就类似如下
var baz = 3; var bazz ={ baz: 2, getbaz: function() { return this.baz } } console.log(bazz.getbaz()) var g = bazz.getbaz; console.log(g()) ;
第一个输出是 2,第二个输出是 3。
这题考察的就是 this 的指向,函数作为对象本身属性调用的时候,this 指向对象,作为普通函数调用的时候,就指向全局了。
还有下面的题:
var arr = [1,2,3,4,5]; for(var i = 0; i < arr.length; i++){ arr[i] = function(){ alert(i) } } arr[3]();
典型的闭包,弹出 5 。
JavaScript 里有哪些数据类型
一、数据类型
- undefiend 没有定义数据类型
- number 数值数据类型,例如 10 或者 1 或者 5.5
- string 字符串数据类型用来描述文本,例如 "你的姓名"
- boolean 布尔类型 true | false ,不是正就是反
- object 对象类型,复杂的一组描述信息的集合
- function 函数类型
解释清楚 null 和 undefined
null 用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。 null 表示"没有对象",即该处不应该有值。
null 典型用法是:
- 作为函数的参数,表示该函数的参数不是对象。
- 作为对象原型链的终点。
当声明的变量还未被初始化时,变量的默认值为 undefined。 undefined 表示"缺少值",就是此处应该有一个值,但是还没有定义。
- 变量被声明了,但没有赋值时,就等于 undefined。
- 调用函数时,应该提供的参数没有提供,该参数等于 undefined。
- 对象没有赋值的属性,该属性的值为 undefined。
- 函数没有返回值时,默认返回 undefined。
未定义的值和定义未赋值的为 undefined,null 是一种特殊的 object,NaN 是一种特殊的 number。
讲一下 1 和 Number(1) 的区别*
- 1 是一个原始定义好的 number 类型;
- Number(1) 是一个函数类型,是我们自己声明的一个函数(方法)。
讲一下 prototype 是什么东西,原型链的理解,什么时候用 prototype ?
prototype 是函数对象上面预设的对象属性。
函数里的 this 什么含义,什么情况下,怎么用 ?
- this 是 Javascript 语言的一个关键字。
- 它代表函数运行时,自动生成的一个内部对象,只能在函数内部使用。
- 随着函数使用场合的不同,this 的值会发生变化。
- 但是有一个总的原则,那就是
this 指的是,调用函数的那个对象
。
情况一:纯粹的函数调用
这是函数的最通常用法,属于全局性调用,因此 this 就代表全局对象 window
。
function test(){ this.x = 1; alert(this.x); } test(); // 1
为了证明 this 就是全局对象,我对代码做一些改变:
var x = 1; function test(){ alert(this.x); } test(); // 1
运行结果还是 1。
再变一下:
var x = 1; function test(){ this.x = 0; } test(); alert(x); // 0
情况二:作为对象方法的调用
函数还可以作为某个对象的方法调用,这时 this 就指这个上级对象
。
function test(){ alert(this.x); } var x = 2 var o = {}; o.x = 1; o.m = test; o.m(); // 1
情况三: 作为构造函数调用
所谓构造函数,就是通过这个函数生成一个新对象(object)。这时的 this 就指这个新对象。
function Test(){ this.x = 1; } var o = new Test(); alert(o.x); // 1
运行结果为 1。为了表明这时 this 不是全局对象,对代码做一些改变:
var x = 2; function Test(){ this.x = 1; } var o = new Test(); alert(x); // 2
运行结果为 2,表明全局变量 x 的值没变。
情况四: apply 调用
apply() 是函数对象的一个方法,它的作用是改变函数的调用对象,它的第一个参数就表示改变后的调用这个函数的对象。因此,this 指的就是这第一个参数。
var x = 0; function test(){ alert(this.x); } var o = {}; o.x = 1; o.m = test; o.m.apply(); // 0
apply() 的参数为空时,默认调用全局对象。因此,这时的运行结果为 0,证明 this 指的是全局对象。
如果把最后一行代码修改为
o.m.apply(o); // 1
运行结果就变成了 1,证明了这时 this 代表的是对象 o。
apply 和 call 什么含义,什么区别 ?什么时候用 ?
call,apply 都属于 Function.prototype 的一个方法,它是 JavaScript 引擎内在实现的,因为属于 Function.prototype,所以每个 Function 对象实例(就是每个方法)都有 call,apply 属性。
既然作为方法的属性,那它们的使用就当然是针对方法的了,这两个方法是容易混淆的,因为它们的作用一样,只是使用方式不同。
语法:
foo.call(this, arg1, arg2, arg3) == foo.apply(this, arguments) == this.foo(arg1, arg2, arg3);
- 相同点:两个方法产生的作用是完全一样的。
- 不同点:方法传递的参数不同。
每个函数对象会有一些方法可以去修改函数执行时里面的 this,比较常见得到就是 call 和 apply,通过 call 和 apply 可以重新定义函数的执行环境,即 this 的指向。
function add(c, d) { console.log(this.a + this.b + c + d); } var o = { a: 1, b: 3 }; add.call(o, 5, 7); //1+3+5+7=16 //传参的时候是扁平的把每个参数传进去 add.apply(o, [10, 20]); //1+3+10+20=34 //传参的时候是把参数作为一个数组传进去 //什么时候使用 call 或者 apply function bar() { console.log(Object.prototype.toString.call(this)); // 用来调用一些无法直接调用的方法 } bar.call(7); // "[object Number]"
异步过程的构成要素有哪些?和异步过程是怎样的 ?
总结一下,一个异步过程通常是这样的:
- 主线程发起一个异步请求,相应的工作线程接收请求并告知主线程已收到(异步函数返回);
- 主线程可以继续执行后面的代码,同时工作线程执行异步任务;
- 工作线程完成工作后,通知主线程;
- 主线程收到通知后,执行一定的动作(调用回调函数)。
- 异步函数通常具有以下的形式:A(args..., callbackFn)。
- 它可以叫做异步过程的发起函数,或者叫做异步任务注册函数。
- args 和 callbackFn 是这个函数的参数。
所以,从主线程的角度看,一个异步过程包括下面两个要素:
- 发起函数(或叫注册函数) A。
- 回调函数 callbackFn。
它们都是在主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果。
举个具体的例子:
setTimeout(fn, 1000);
其中的 setTimeout 就是异步过程的发起函数,fn 是回调函数。
注意:前面说的形式 A(args..., callbackFn) 只是一种抽象的表示,并不代表回调函数一定要作为发起函数的参数。
例如:
var xhr = new XMLHttpRequest(); xhr.onreadystatechange = xxx; // 添加回调函数 xhr.open('GET', url); xhr.send(); // 发起函数
发起函数和回调函数就是分离的。
说说消息队列和事件循环
- 主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息(也就是 message 函数),并执行它。
- 完成了工作线程对主线程的通知,回调函数也就得到了执行。
- 如果一开始主线程就没有提供回调函数,AJAX 线程在收到 HTTP 响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。
异步过程的回调函数,一定不在当前的这一轮事件循环中执行。