本文讲解 56 道 JavaScript 和 ES6+ 面试题的内容。
复习前端面试的知识,是为了巩固前端的基础知识,最重要的还是平时的积累!
注意
:文章的题与题之间用下划线分隔开,答案仅供参考。
前端硬核面试专题的完整版在此:前端硬核面试专题,包含:HTML + CSS + JS + ES6 + Webpack + Vue + React + Node + HTTPS + 数据结构与算法 + Git 。
“我自己是一名从事了8年web前端开发的老程序员(我的微信:webxxq),今年年初我花了一个月整理了一份最适合2020年自学的web前端全套培训教程(视频+笔记+素材+源码+项目实战),从最基础的HTML+CSS+JS到移动端HTML5以及各种框架和新技术都有整理,打包给每一位前端小伙伴(总共约85G),这里是前端学习者聚集地,欢迎初学和进阶中的小伙伴(所有前端教程关注我的微信公众号:web前端学习圈,关注后回复“2020”即可领取)。
常见的浏览器内核有哪些 ?
mouseenter 和 mouseover 的区别
用正则表达式匹配字符串,以字母开头,后面是数字、字符串或者下划线,长度为 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(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收。
线程与进程的区别 ?
线程在执行过程中与进程还是有区别的。
但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
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
alert(0 || 1) 的结果是 1
只要记住 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 里有哪些数据类型
一、数据类型
解释清楚 null 和 undefined
null 用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。 null 表示"没有对象",即该处不应该有值。
null 典型用法是:
当声明的变量还未被初始化时,变量的默认值为 undefined。 undefined 表示"缺少值",就是此处应该有一个值,但是还没有定义。
未定义的值和定义未赋值的为 undefined,null 是一种特殊的 object,NaN 是一种特殊的 number。
讲一下 1 和 Number(1) 的区别*
讲一下 prototype 是什么东西,原型链的理解,什么时候用 prototype ?
prototype 是函数对象上面预设的对象属性。
函数里的 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]"
异步过程的构成要素有哪些?和异步过程是怎样的 ?
总结一下,一个异步过程通常是这样的:
所以,从主线程的角度看,一个异步过程包括下面两个要素:
它们都是在主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果。
举个具体的例子:
setTimeout(fn, 1000);
其中的 setTimeout 就是异步过程的发起函数,fn 是回调函数。
注意:前面说的形式 A(args..., callbackFn) 只是一种抽象的表示,并不代表回调函数一定要作为发起函数的参数。
例如:
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回调函数
xhr.open('GET', url);
xhr.send(); // 发起函数
发起函数和回调函数就是分离的。
说说消息队列和事件循环
异步过程的回调函数,一定不在当前的这一轮事件循环中执行。
session 与 cookie 的区别
cookies 是干嘛的,服务器和浏览器之间的 cookies 是怎么传的,httponly 的 cookies 和可读写的 cookie 有什么区别,有无长度限制 ?
请描述一下 cookies,sessionStorage 和 localStorage 的区别
共同点
区别
不共享
,即使是同一个页面;cookie 和 localStorage 在所有同源窗口中都是共享的。从敲入 URL 到渲染完成的整个过程,包括 DOM 构建的过程,说的约详细越好
详情:面试题之从敲入 URL 到浏览器渲染完成
是否了解公钥加密和私钥加密。如何确保表单提交里的密码字段不被泄露。
公钥用于对数据进行加密,私钥用于对数据进行解密。
很直观的理解:公钥就是公开的密钥,其公开了大家才能用它来加密数据。私钥是私有的密钥,谁有这个密钥才能够解密密文。
解决方案 1:
form 在提交的过程中,对密码字段是不进行加密而是以明码的形式进行数据传输的。
如果要对数据进行加密,你可以自己写一个脚本对内容进行编码后传输,只是这个安全性也并不高。
解决方案 2:
如果想对数据进行加密,你可以使用 HTTPS 安全传输协议,这个协议是由系统进行密码加密处理的,在数据传输中是绝对不会被拦截获取的,只是 HTTPS 的架设会相对麻烦点。一些大型网站的登录、银行的在线网关等都是走这条路。
验证码是干嘛的,是为了解决什么安全问题。
所谓验证码,就是将一串随机产生的数字或符号,生成一幅图片, 图片里加上一些干扰象素(防止OCR),由用户肉眼识别其中的验证码信息,输入表单提交网站验证,验证成功后才能使用某项功能。
截取字符串 abcdefg 的 efg。
从第四位开始截取
alert('abcdefg'.substring(4));
alert ('abcdefg'.slice(4))
判断一个字符串中出现次数最多的字符,统计这个次数
步骤
var str = 'abaasdffggghhjjkkgfddsssss3444343';
// 1.将字符串转换成数组
var newArr = str.split("");
// 2.创建一个对象
var json = {};
// 3. 所有字母出现的次数,判断对象中是否存在数组中的值,如果存在值 +1,不存在赋值为 1
for(var i = 0; i < newArr.length; i++){
// 类似:json : { ‘a’: 3, ’b’: 1 }
if(json[newArr[i]]){
json[newArr[i]] +=1;
} else {
json[newArr[i]] = 1;
}
}
// 4 定义两个变量存储字符值,字符出现的字数
var num = 0 ; //次数
var element = ""; //最多的项
for(var k in json){
if(json[k] > num){
num = json[k];
element = k ;
}
}
console.log("出现次数:"+num +"最多的字符:"+ element);
document.write 和 innerHTML 的区别
JS 识别不同浏览器信息
function myBrowser() {
var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串
var isOpera = userAgent.indexOf("Opera") > -1;
if (isOpera) {
return "Opera"
}; //判断是否Opera浏览器
if (userAgent.indexOf("Firefox") > -1) {
return "Firefox";
} //判断是否Firefox浏览器
if (userAgent.indexOf("Chrome") > -1) {
return "Chrome";
} //判断是否Google浏览器
if (userAgent.indexOf("Safari") > -1) {
return "Safari";
} //判断是否Safari浏览器
if (userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1 && !isOpera) {
return "IE";
}; //判断是否IE浏览器
}
JavaScript 常见的内置对象
有 Object、Math、String、Array、Number、Function、Boolean、JSON 等,其中 Object 是所有对象的基类,采用了原型继承方式。
编写一个方法,求一个字符串的字节长度
假设:一个英文字符占用一个字节,一个中文字符占用两个字节
function getBytes(str){
var len = str.length;
var bytes = len;
for(var i = 0; i < len; i++){
if (str.charCodeAt(i) > 255) bytes++;
}
return bytes;
}
alert(getBytes("你好,as"));
JS 组成
new 操作符具体干了什么呢 ?
JSON 的了解?
你有哪些性能优化的方法 ?
web 前端是应用服务器处理之前的部分,前端主要包括:HTML、CSS、javascript、image 等各种资源,针对不同的资源有不同的优化方式。
内容优化
常见方法:合并多个 CSS 文件和 js 文件,利用 CSS Sprites 整合图像,Inline Images (使用 data:URL scheme 在实际的页面嵌入图像数据 ),合理设置 HTTP 缓存等。
服务器优化
Cookie 优化
CSS 优化
javascript 优化
在实际应用中使用外部文件可以提高页面速度,因为 JavaScript 和 CSS 文件都能在浏览器中产生缓存。
图像优化
JS 格式化数字(每三位加逗号)
从后往前取。
function toThousands(num) {
var num = (num || 0).toString(), result = '';
while (num.length > 3) {
result = ',' + num.slice(-3) + result;
num = num.slice(0, num.length - 3);
}
if (num) { result = num + result; }
return result;
}
合并数组
如果你需要合并两个数组的话,可以使用 Array.concat()
var array1 = [1, 2, 3];
var array2 = [4, 5, 6];
console.log(array1.concat(array2)); // [1,2,3,4,5,6];
然而,这个函数并不适用于合并大的数组,因为它需要创建一个新的数组,而这会消耗很多内存。
这时,你可以使用 Array.push.apply(arr1, arr2) 来代替创建新的数组,它可以把第二个数组合并到第一个中,从而较少内存消耗。
var array1 = [1, 2, 3];
var array2 = [4, 5, 6];
console.log(array1.push.apply(array1, array2)); // [1, 2, 3, 4, 5, 6]
把节点列表 (NodeList) 转换为数组
如果你运行 document.querySelectorAll("p") 方法,它可能会返回一个 DOM 元素的数组 — 节点列表对象。
但这个对象并不具有数组的全部方法,如 sort(),reduce(), map(),filter()。
为了使用数组的那些方法,你需要把它转换为数组。
只需使用 [].slice.call(elements) 即可实现:
var elements = document.querySelectorAll("p"); // NodeList
var arrayElements = [].slice.call(elements); // 现在 NodeList 是一个数组
var arrayElements = Array.from(elements); // 这是另一种转换 NodeList 到 Array 的方法
打乱数组元素的顺序
不适用 Lodash 等这些库打乱数组元素顺序,你可以使用这个技巧:
var list = [1, 2, 3];
console.log(list.sort(function() { Math.random() - 0.5 })); // [2, 1, 3]
js 的 ready 和 onload 事件的区别
js 的两种回收机制
标记清除(mark and sweep)
从语义上理解就比较好理解了,大概就是当变量进入到某个环境中的时候就把这个变量标记一下,比如标记为“进入环境”,当离开的时候就把这个变量的标记给清除掉,比如是“离开环境”。而在这后面还有标记的变量将被视为准备删除的变量。
这是 javascript 最常见的垃圾回收方式。至于上面有说道的标记,到底该如何标记 ?
好像是有很多方法,比如特殊位翻转,维护一个列表什么的。
引用计数(reference counting)
三张图搞懂 JavaScript 的原型对象与原型链
对于新人来说,JavaScript 的原型是一个很让人头疼的事情,一来 prototype 容易与 proto 混淆,
一、prototype 和 proto 的区别
var a = {};
console.log(a.prototype); //undefined
console.log(a.__proto__); //Object {}
var b = function(){}
console.log(b.prototype); //b {}
console.log(b.__proto__); //function() {}
结果:
/*1、字面量方式*/
var a = {};
console.log("a.__proto__ :", a.__proto__); // Object {}
console.log("a.__proto__ === a.constructor.prototype:", a.__proto__ === a.constructor.prototype); // true
/*2、构造器方式*/
var A = function(){};
var a2 = new A();
console.log("a2.__proto__:", a2.__proto__); // A {}
console.log("a2.__proto__ === a2.constructor.prototype:", a2.__proto__ === a2.constructor.prototype); // true
/*3、Object.create()方式*/
var a4 = { a: 1 }
var a3 = Object.create(a4);
console.log("a3.__proto__:", a3.__proto__); // Object {a: 1}
console.log("a3.__proto__ === a3.constructor.prototype:", a3.__proto__ === a3.constructor.prototype); // false(此处即为图1中的例外情况)
结果:
var A = function(){};
var a = new A();
console.log(a.__proto__); // A {}(即构造器 function A 的原型对象)
console.log(a.__proto__.__proto__); // Object {}(即构造器 function Object 的原型对象)
console.log(a.__proto__.__proto__.__proto__); // null
结果:
闭包的理解 ?
一、变量的作用域
要理解闭包,首先必须理解 Javascript 特殊的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。
Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。
var n = 999;
function f1(){
alert(n);
}
f1(); // 999
另一方面,在函数外部自然无法读取函数内的局部变量。
function f1(){
var n = 999;
}
alert(n); // error
这里有一个地方需要注意,函数内部声明变量的时候,一定要使用 var 命令。
如果不用的话,你实际上声明了一个全局变量!
function f1(){
n = 999;
}
f1();
alert(n); // 999
二、如何从外部读取局部变量 ?
function f1() {
var n = 999;
function f2() {
alert(n);
}
return f2;
}
var result = f1();
result(); // 999
既然 f2 可以读取 f1 中的局部变量,那么只要把 f2 作为返回值,我们不就可以在 f1 外部读取它的内部变量了吗!
三、闭包的概念
上一节代码中的 f2 函数,就是闭包。
我的理解是,闭包就是能够读取其他函数内部变量的函数
。
由于在 Javascript 语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成 定义在一个函数内部的函数
。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁
。
四、闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
怎么来理解呢 ?请看下面的代码。
function f1() {
var n = 999;
nAdd = function () { n += 1 }
function f2() {
alert(n);
}
return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000
在这段代码中,result 实际上就是闭包 f2 函数。它一共运行了两次,第一次的值是 999,第二次的值是 1000。这证明了,函数 f1 中的局部变量 n 一直保存在内存中,并没有在 f1 调用后被自动清除。
为什么会这样呢 ?
原因就在于 f1 是 f2 的父函数,而 f2 被赋给了一个全局变量,这导致 f2 始终在内存中,而 f2 的存在依赖于 f1,因此 f1 也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是
五、使用闭包的注意点
闭包面试经典问题
问题:想每次点击对应目标时弹出对应的数字下标 0~4 ,但实际是无论点击哪个目标都会弹出数字 5。
function onMyLoad() {
var arr = document.getElementsByTagName("p");
for (var i = 0; i < arr.length; i++) {
arr[i].onclick = function () {
alert(i);
}
}
}
问题所在:arr 中的每一项的 onclick 均为一个函数实例(Function 对象),这个函数实例也产生了一个闭包域,这个闭包域引用了外部闭包域的变量,其 function scope 的 closure 对象有个名为 i 的引用,外部闭包域的私有变量内容发生变化,内部闭包域得到的值自然会发生改变。
解决办法一
解决思路:增加若干个对应的闭包域空间(这里采用的是匿名函数),专门用来存储原先需要引用的内容(下标),不过只限于基本类型(基本类型值传递,对象类型引用传递)。
//声明一个匿名函数,若传进来的是基本类型则为值传递,故不会对实参产生影响,
//该函数对象有一个本地私有变量 arg(形参) ,该函数的 function scope 的 closure 对象属性有两个引用,一个是 arr,一个是 i
//尽管引用 i 的值随外部改变 ,但本地私有变量(形参) arg 不会受影响,其值在一开始被调用的时候就决定了
for (var i = 0; i < arr.length; i++) {
(function (arg) {
arr[i].onclick = function () {
// onclick 函数实例的 function scope 的 closure 对象属性有一个引用 arg,
alert(arg);
//只要 外部空间的 arg 不变,这里的引用值当然不会改变
}
})(i); //立刻执行该匿名函数,传递下标 i (实参)
}
解决办法二
解决思路:将事件绑定在新增的匿名函数返回的函数上,此时绑定的函数中的 function scope 中的 closure 对象的 引用 arg 是指向将其返回的匿名函数的私有变量 arg
for (var i = 0; i < arr.length; i++) {
arr[i].onclick = (function (arg) {
return function () {
alert(arg);
}
})(i);
}
解决办法三
使用 ES6 新语法 let 关键字
for (var i = 0; i < arr.length; i++) {
let j = i; // 创建一个块级变量
arr[i].onclick = function () {
alert(j);
}
}
JavaScript 判断一个变量是对象还是数组 ?
typeof 都返回 object
在 JavaScript 中所有数据类型严格意义上都是对象,但实际使用中我们还是有类型之分,如果要判断一个变量是数组还是对象使用 typeof 搞不定,因为它全都返回 object。
第一,使用 typeof 加 length 属性
数组有 length 属性,object 没有,而 typeof 数组与对象都返回 object,所以我们可以这么判断
var getDataType = function(o){
if(typeof o == 'object'){
if( typeof o.length == 'number' ){
return 'Array';
} else {
return 'Object';
}
} else {
return 'param is no object type';
}
};
第二,使用 instanceof
利用 instanceof 判断数据类型是对象还是数组时应该优先判断 array,最后判断 object。
var getDataType = function(o){
if(o instanceof Array){
return 'Array'
} else if ( o instanceof Object ){
return 'Object';
} else {
return 'param is no object type';
}
};
ES5 的继承和 ES6 的继承有什么区别 ?
ES5 的继承时通过 prototype 或构造函数机制来实现。
ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.apply(this))
。ES6 的继承机制完全不同,实质上是先创建父类的实例对象 this(所以必须先调用父类的 super()方法),然后再用子类的构造函数修改 this
。具体的:ES6 通过 class 关键字定义类,里面有构造方法,类之间通过 extends 关键字实现继承。子类必须在 constructor 方法中调用 super 方法,否则新建实例报错。因为子类没有自己的 this 对象,而是继承了父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类得不到 this 对象。
ps:super 关键字指代父类的实例,即父类的 this 对象。在子类构造函数中,调用 super 后,才可使用 this 关键字,否则报错。
翻转一个字符串
先将字符串转成一个数组,然后用数组的 reverse() + join() 方法。
let a = "hello word";
let b = [...str].reverse().join(""); // drow olleh
说说堆和栈的区别 ?
一、堆栈空间分配区别
二、堆栈缓存方式区别
三、堆栈数据结构区别
js 经典面试知识文章
ES6 声明变量的六种方法
Promise 的队列与 setTimeout 的队列有何关联 ?
setTimeout(function(){ console.log(4) }, 0);
new Promise(function(resolve){
console.log(1)
for( var i = 0 ; i < 10000 ; i++ ){
i == 9999 && resolve()
}
console.log(2)
}).then(function(){
console.log(5)
});
console.log(3);
为什么结果是:1, 2, 3, 5, 4;而不是:1, 2, 3, 4, 5 ?
js 里面有宏任务(macrotask)和微任务(microtask)。
因为 setTimeout 是属于 macrotask 的,而整个 script 也是属于一个 macrotask,promise.then 回调是 microtask,执行过程大概如下:
ES6+ 面试知识文章
前端硬核面试专题的完整版在此:前端硬核面试专题,包含:HTML + CSS + JS + ES6 + Webpack + Vue + React + Node + HTTPS + 数据结构与算法 + Git 。
如果觉得本文还不错,记得给个 star , 你的 star 是我持续更新的动力!。