企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
https://github.com/yisainan/web-interview/edit/master/content/js/js.md 现在开发主要都是搞 vue react 这些了, dom 有能力就学, 没能力就先放着,不熟也影响不大 js 的面向对象能学就学, 没能力就先学学原型链, class 和继承这些可以先放放 ## document load 和 document ready 的区别 *** ``` 页面加载完成有两种事件 1.load是当页面所有资源全部加载完成后(包括DOM文档树,css文件,js文件,图片资源等),执行一个函数 问题:如果图片资源较多,加载时间较长,onload后等待执行的函数需要等待较长时间,所以一些效果可能受到影响 2.$(document).ready()是当DOM文档树加载完成后执行一个函数 (不包含图片,css等)所以会比load较快执行 在原生的js中不包括ready()这个方法,只有load方法也就是onload事件 ``` ## JavaScript 中如何检测一个变量是一个 String 类型?**** ``` 参考答案:三种方法(typeof、constructor、Object.prototype.toString.call()) 解析: ① typeof typeof('123') === "string" // true typeof '123' === "string" // true ② constructor '123'.constructor === String // true ③ Object.prototype.toString.call() Object.prototype.toString.call('123') === '[object String]' // true ``` ## 请用 js 去除字符串空格? ** 面试能说个就行 参考答案:replace 正则匹配方法、str.trim()方法、JQ 方法:$.trim(str)方法 解析: 方法一:replace 正则匹配方法 去除字符串内所有的空格:str = str.replace(/\\s\*/g, ""); 去除字符串内两头的空格:str = str.replace(/^\\s\*|\\s\*$/g, ""); 去除字符串内左侧的空格:str = str.replace(/^\\s\*/, ""); 去除字符串内右侧的空格:str = str.replace(/(\\s\*$)/g, ""); 示例: ~~~js var str = " 6 6 "; var str_1 = str.replace(/\s*/g, ""); console.log(str_1); //66 var str = " 6 6 "; var str_1 = str.replace(/^\s*|\s*$/g, ""); console.log(str_1); //6 6//输出左右侧均无空格 var str = " 6 6 "; var str_1 = str.replace(/^\s*/, ""); console.log(str_1); //6 6 //输出右侧有空格左侧无空格 var str = " 6 6 "; var str_1 = str.replace(/(\s*$)/g, ""); console.log(str_1); // 6 6//输出左侧有空格右侧无空格 ~~~ 方法二:str.trim()方法 trim()方法是用来删除字符串两端的空白字符并返回,trim 方法并不影响原来的字符串本身,它返回的是一个新的字符串。 缺陷:只能去除字符串两端的空格,不能去除中间的空格 示例: ~~~js var str = " 6 6 "; var str_1 = str.trim(); console.log(str_1); //6 6//输出左右侧均无空格 ~~~ 方法三:JQ 方法:$.trim(str)方法 $.trim() 函数用于去除字符串两端的空白字符。 注意:$.trim()函数会移除字符串开始和末尾处的所有换行符,空格(包括连续的空格)和制表符。如果这些空白字符在字符串中间时,它们将被保留,不会被移除。 示例: ~~~js var str = " 6 6 "; var str_1 = $.trim(str); console.log(str_1); //6 6//输出左右侧均无空格 ~~~ ## == 和 === 的不同 ***** 参考答案:`==`是抽象相等运算符,而`===`是严格相等运算符。`==`运算符是在进行必要的类型转换后,再比较。`===`运算符不会进行类型转换,所以如果两个值不是相同的类型,会直接返回`false`。使用`==`时,可能发生一些特别的事情,例如: ~~~js 1 == "1"; // true 1 == [1]; // true 1 == true; // true 0 == ""; // true 0 == "0"; // true 0 == false; // true ~~~ 如果你对`==`和`===`的概念不是特别了解,建议大多数情况下使用`===` ## 怎样添加、移除、移动、复制、创建和查找节点?* 参考答案: 1)创建新节点 createDocumentFragment() //创建一个 DOM 片段 createElement() //创建一个具体的元素 createTextNode() //创建一个文本节点 2)添加、移除、替换、插入 appendChild() //添加 removeChild() //移除 replaceChild() //替换 insertBefore() //插入 3)查找 getElementsByTagName() //通过标签名称 getElementsByName() //通过元素的 Name 属性的值 getElementById() //通过元素 Id,唯一性 ### 事件委托是什么 **** 参考答案:利用事件冒泡的原理,让自己的所触发的事件,让他的父元素代替执行! 解析: 1、那什么样的事件可以用事件委托,什么样的事件不可以用呢? * 适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。 * 值得注意的是,mouseover 和 mouseout 虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。 * 不适合的就有很多了,举个例子,mousemove,每次都要计算它的位置,非常不好把控,在不如说 focus,blur 之类的,本身就没用冒泡的特性,自然就不用事件委托了。 2、为什么要用事件委托 * 1.提高性能 ~~~ <ul>  <li>苹果</li>  <li>香蕉</li>  <li>凤梨</li> </ul> // good document.querySelector('ul').onclick = (event) => { let target = event.target if (target.nodeName === 'LI') { console.log(target.innerHTML) } } // bad document.querySelectorAll('li').forEach((e) => { e.onclick = function() { console.log(this.innerHTML) } }) ~~~ * 2.新添加的元素还会有之前的事件。 3、事件冒泡与事件委托的对比 * 事件冒泡:box 内部无论是什么元素,点击后都会触发 box 的点击事件 * 事件委托:可以对 box 内部的元素进行筛选 4、事件委托怎么取索引? ~~~html <ul id="ul"> <li> aaaaaaaa </li> <li> 事件委托了 点击当前, 如何获取 这个点击的下标 </li> <li> cccccccc </li> </ul> ~~~ ~~~js window.onload = function() { var oUl = document.getElementById("ul"); var aLi = oUl.getElementsByTagName("li"); oUl.onclick = function(ev) { var ev = ev || window.event; var target = ev.target || ev.srcElement; if (target.nodeName.toLowerCase() == "li") { var that = target; var index; for (var i = 0; i < aLi.length; i++) if (aLi[i] === target) index = i; if (index >= 0) alert('我的下标是第' + index + '个'); target.style.background = "red"; } } } ~~~ 拓展: * 键盘事件:keydown keypress keyup * 鼠标事件:mousedown mouseup mousemove mouseout mouseover [参考](https://github.com/qiilee/js/tree/master/JS/%E4%BA%8B%E4%BB%B6%E5%A7%94%E6%89%98) ## require 与 import 的区别 **** 参考答案:两者的加载方式不同、规范不同 第一、两者的加载方式不同,require 是在运行时加载,而 import 是在编译时加载 require('./a')(); // a 模块是一个函数,立即执行 a 模块函数 var data = require('./a').data; // a 模块导出的是一个对象 var a = require('./a')\[0\]; // a 模块导出的是一个数组 ======> 哪都行 import $ from 'jquery'; import \* as \_ from '\_'; import {a, b, c} from './a'; import {default as alias, a as a\_a, b, c} from './a'; ======>用在开头 第二、规范不同,require 是 CommonJS/AMD 规范,import 是 ESMAScript6+规范 第三、require 特点:社区方案,提供了服务器/浏览器的模块加载方案。非语言层面的标准。只能在运行时确定模块的依赖关系及输入/输出的变量,无法进行静态优化。 import 特点:语言规格层面支持模块功能。支持编译时静态分析,便于 JS 引入宏和类型检验。动态绑定。 ## javascript 对象的几种创建方式 *** 参考答案: 第一种:Object 构造函数创建 ~~~js var Person = new Object(); Person.name = "Nike"; Person.age = 29; ~~~ 这行代码创建了 Object 引用类型的一个新实例,然后把实例保存在变量 Person 中。 第二种:使用对象字面量表示法 ~~~js var Person = {}; //相当于 var Person = new Object(); var Person = { name: 'Nike'; age: 29; } ~~~ 对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程。也就是说,第一种和第二种方式创建对象的方法其实都是一样的,只是写法上的区别不同 在介绍第三种的创建方法之前,我们应该要明白为什么还要用别的方法来创建对象,也就是第一种,第二种方法的缺点所在:它们都是用了同一个接口创建很多对象,会产生大量的重复代码,就是如果你有 100 个对象,那你要输入 100 次很多相同的代码。那我们有什么方法来避免过多的重复代码呢,就是把创建对象的过程封装在函数体内,通过函数的调用直接生成对象。 第三种:使用工厂模式创建对象 ~~~js function createPerson(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function() { alert(this.name); }; return o; } var person1 = createPerson("Nike", 29, "teacher"); var person2 = createPerson("Arvin", 20, "student"); ~~~ 在使用工厂模式创建对象的时候,我们都可以注意到,在 createPerson 函数中,返回的是一个对象。那么我们就无法判断返回的对象究竟是一个什么样的类型。于是就出现了第四种创建对象的模式。 第四种: 使用构造函数创建对象 ~~~js function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { alert(this.name); }; } var person1 = new Person("Nike", 29, "teacher"); var person2 = new Person("Arvin", 20, "student"); ~~~ 对比工厂模式,我们可以发现以下区别: 1.没有显示地创建对象 2.直接将属性和方法赋给了 this 对象 3.没有 return 语句 4.终于可以识别的对象的类型。对于检测对象类型,我们应该使用 instanceof 操作符,我们来进行自主检测: ~~~js alert(person1 instanceof Object); //ture alert(person1 instanceof Person); //ture alert(person2 instanceof Object); //ture alert(person2 instanceof Object); //ture ~~~ 同时我们也应该明白,按照惯例,构造函数始终要应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。 那么构造函数确实挺好用的,但是它也有它的缺点: 就是每个方法都要在每个实例上重新创建一遍,方法指的就是我们在对象里面定义的函数。如果方法的数量很多,就会占用很多不必要的内存。于是出现了第五种创建对象的方法 第五种:原型创建对象模式 ~~~js function Person() {} Person.prototype.name = "Nike"; Person.prototype.age = 20; Person.prototype.jbo = "teacher"; Person.prototype.sayName = function() { alert(this.name); }; var person1 = new Person(); person1.sayName(); ~~~ 使用原型创建对象的方式,可以让所有对象实例共享它所包含的属性和方法。 如果是使用原型创建对象模式,请看下面代码: ~~~js function Person() {} Person.prototype.name = "Nike"; Person.prototype.age = 20; Person.prototype.jbo = "teacher"; Person.prototype.sayName = function() { alert(this.name); }; var person1 = new Person(); var person2 = new Person(); person1.name = "Greg"; alert(person1.name); //'Greg' --来自实例 alert(person2.name); //'Nike' --来自原型 ~~~ 当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。 这时候我们就可以使用构造函数模式与原型模式结合的方式,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性 第六种:组合使用构造函数模式和原型模式 ~~~js function Person(name, age, job) { this.name = name; this.age = age; this.job = job; } Person.prototype = { constructor: Person, sayName: function() { alert(this.name); }; } var person1 = new Person('Nike', 20, 'teacher'); ~~~ 解析:[参考](https://zhidao.baidu.com/question/1180348878138910499.html) ## JavaScript 继承的方式和优缺点 *** 参考答案:六种方式 * 一、原型链继承 * 缺点: * 1.引用类型的属性被所有实例共享 * 2.在创建 Child 的实例时,不能向 Parent 传参 * 二、借用构造函数(经典继承) * 优点: * 1.避免了引用类型的属性被所有实例共享 * 2.可以在 Child 中向 Parent 传参 * 缺点: * 1.方法都在构造函数中定义,每次创建实例都会创建一遍方法。 * 三、组合继承 * 优点: * 1.融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。 * 四、原型式继承 * 缺点: * 1.包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。 * 五、寄生式继承 * 缺点: * 1.跟借用构造函数模式一样,每次创建对象都会创建一遍方法。 * 六、寄生组合式继承 * 优点: * 1.这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。 * 2.与此同时,原型链还能保持不变; * 3.因此,还能够正常使用 instanceof 和 isPrototypeOf。 * 开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式 解析:[参考](https://www.jianshu.com/p/09ad43c7fe8f) ## 什么是原型链?***** 参考答案:通过一个对象的\_\_proto\_\_可以找到它的原型对象,原型对象也是一个对象,就可以通过原型对象的\_\_proto\_\_,最后找到了我们的 Object.prototype, 从实例的原型对象开始一直到 Object.prototype 就是我们的原型链 解析: [![js_001](https://github.com/yisainan/web-interview/raw/master/images/js_001.png)](https://github.com/yisainan/web-interview/blob/master/images/js_001.png) ## 复杂数据类型如何转变为字符串 **** 参考答案: * 首先,会调用 valueOf 方法,如果方法的返回值是一个基本数据类型,就返回这个值, * 如果调用 valueOf 方法之后的返回值仍旧是一个复杂数据类型,就会调用该对象的 toString 方法, * 如果 toString 方法调用之后的返回值是一个基本数据类型,就返回这个值, * 如果 toString 方法调用之后的返回值是一个复杂数据类型,就报一个错误。 解析: ~~~js // 1; var obj = { valueOf: function() { return 1; } }; console.log(obj + ""); //'1' // 2; var obj = { valueOf: function() { return [1, 2]; } }; console.log(obj + ""); //'[object Object]'; // 3; var obj = { valueOf: function() { return [1, 2]; }, toString: function() { return 1; } }; console.log(obj + ""); //'1'; // 4; var obj = { valueOf: function() { return [1, 2]; }, toString: function() { return [1, 2, 3]; } }; console.log(obj + ""); // 报错 Uncaught TypeError: Cannot convert object to primitive value ~~~ 拓展: ~~~js var arr = [new Object(), new Date(), new RegExp(), new String(), new Number(), new Boolean(), new Function(), new Array(), Math] console.log(arr.length) // 9 for (var i = 0; i < arr.length; i++) { arr[i].valueOf = function() { return [1, 2, 3] } arr[i].toString = function() { return 'toString' } console.log(arr[i] + '') } ~~~ 1、若 return \[1, 2, 3\]处为 return "valueof",得到的返回值是 valueof toString 7valueof 说明:其他八种复杂数据类型是先调用 valueOf 方法,时间对象是先调用 toString 方法 2、改成 return \[1, 2, 3\],得到的返回值是 9toString 说明:执行 valueof 后都来执行 toString ## javascript 的 typeof 返回哪些数据类型 ***** 参考答案:7 种分别为 string、boolean、number、Object、Function、undefined、symbol(ES6)、 示例: 1、number ~~~js typeof(10); typeof(NaN); // NaN在JavaScript中代表的是特殊非数字值,它本身是一个数字类型。 typeof(Infinity) ~~~ 2、boolean ~~~js typeof(true); typeof(false); ~~~ 3、string ~~~js typeof("abc"); ~~~ 4、undefined ~~~js typeof(undefined); typeof(a); // 不存在的变量 ~~~ 5、object ~~~js // 对象,数组,null返回object typeof(null); typeof(window); ~~~ 6、function ~~~js typeof(Array); typeof(Date); ~~~ 7、symbol ~~~js typeof Symbol() // ES6提供的新的类型 ~~~ ## 一次js请求一般情况下有哪些地方会有缓存处理?**** 参考答案:DNS缓存,CDN缓存,浏览器缓存,服务器缓存。 解析: #### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#1dns%E7%BC%93%E5%AD%98)1、DNS缓存 DNS缓存指DNS返回了正确的IP之后,系统就会将这个结果临时储存起来。并且它会为缓存设定一个失效时间 (例如N小时),在这N小时之内,当你再次访问这个网站时,系统就会直接从你电脑本地的DNS缓存中把结果交还给你,而不必再去询问DNS服务器,变相“加速”了网址的解析。当然,在超过N小时之后,系统会自动再次去询问DNS服务器获得新的结果。 所以,当你修改了 DNS 服务器,并且不希望电脑继续使用之前的DNS缓存时,就需要手动去清除本地的缓存了。 本地DNS迟迟不生效或者本地dns异常等问题,都会导致访问某些网站出现无法访问的情况,这个时候我们就需要手动清除本地dns缓存,而不是等待! #### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#2cdn%E7%BC%93%E5%AD%98)2、CDN缓存 和Http类似,客户端请求数据时,先从本地缓存查找,如果被请求数据没有过期,拿过来用,如果过期,就向CDN边缘节点发起请求。CDN便会检测被请求的数据是否过期,如果没有过期,就返回数据给客户端,如果过期,CDN再向源站发送请求获取新数据。和买家买货,卖家没货,卖家再进货一个道理^^。 CDN边缘节点缓存机制,一般都遵守http标准协议,通过http响应头中的Cache-Control和max-age的字段来设置CDN边缘节点的数据缓存时间。 #### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#3%E6%B5%8F%E8%A7%88%E5%99%A8%E7%BC%93%E5%AD%98)3、浏览器缓存 浏览器缓存(Browser Caching)是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。 浏览器缓存主要有两类:缓存协商:Last-modified ,Etag 和彻底缓存:cache-control,Expires。浏览器都有对应清除缓存的方法。 #### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#4%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BC%93%E5%AD%98)4、服务器缓存 服务器缓存有助于优化性能和节省宽带,它将需要频繁访问的Web页面和对象保存在离用户更近的系统中,当再次访问这些对象的时候加快了速度。 ## 列举 3 种强制类型转换和 2 种隐式类型转换 **** 参考答案: 强制: parseInt(), parseFloat(), Number(), Boolean(), String() 隐式: +, - 解析: ~~~js // 1.parseInt() 把值转换成整数 parseInt("1234blue"); // 1234 parseInt("0xA"); // 10 parseInt("22.5"); // 22 parseInt("blue"); // NaN // parseInt()方法还有基模式,可以把二进制、八进制、十六进制或其他任何进制的字符串转换成整数。基是由parseInt()方法的第二个参数指定的,示例如下: parseInt("AF", 16); // 175 parseInt("10", 2); // 2 parseInt("10", 8); // 8 parseInt("10", 10); // 10 // 如果十进制数包含前导0,那么最好采用基数10,这样才不会意外地得到八进制的值。例如: parseInt("010"); // 8 parseInt("010", 8); // 8 parseInt("010", 10); // 10 // 2.parseFloat() 把值转换成浮点数,没有基模式 parseFloat("1234blue"); // 1234.0 parseFloat("0xA"); // NaN parseFloat("22.5"); // 22.5 parseFloat("22.34.5"); // 22.34 parseFloat("0908"); // 908 parseFloat("blue"); // NaN // 3.Number() 把给定的值转换成数字(可以是整数或浮点数),Number()的强制类型转换与parseInt()和parseFloat()方法的处理方式相似,只是它转换的是整个值,而不是部分值。示例如下: Number(false) // 0 Number(true) // 1 Number(undefined) // NaN Number(null) // 0 Number("5.5") // 5.5 Number("56") // 56 Number("5.6.7") // NaN Number(new Object()) // NaN Number(100) // 100 // 4.Boolean() 把给定的值转换成Boolean型 Boolean(""); // false Boolean("hi"); // true Boolean(100); // true Boolean(null); // false Boolean(0); // false Boolean(new Object()); // true // 5.String() 把给定的值转换成字符串 String(123) // "123" // 6.+ - console.log(0 + '1') // "01" console.log(2 - '1') // 1 ~~~ ## 你对闭包的理解?优缺点?***** 参考答案: 概念:闭包就是能够读取其他函数内部变量的函数。 三大特性: * 函数嵌套函数。 * 函数内部可以引用外部的参数和变量。 * 参数和变量不会被垃圾回收机制回收。 优点: * 希望一个变量长期存储在内存中。 * 避免全局变量的污染。 * 私有成员的存在。 缺点: * 常驻内存,增加内存使用量。 * 使用不当会很容易造成内存泄露。 示例: ~~~js function outer() { var name = "jack"; function inner() { console.log(name); } return inner; } outer()(); // jack ~~~ ~~~js function sayHi(name) { return () => { console.log(`Hi! ${name}`); }; } const test = sayHi("xiaoming"); test(); // Hi! xiaoming ~~~ 虽然 sayHi 函数已经执行完毕,但是其活动对象也不会被销毁,因为 test 函数仍然引用着 sayHi 函数中的变量 name,这就是闭包。 但也因为闭包引用着另一个函数的变量,导致另一个函数已经不使用了也无法销毁,所以闭包使用过多,会占用较多的内存,这也是一个副作用。 解析: 由于在 ECMA2015 中,只有函数才能分割作用域,函数内部可以访问当前作用域的变量,但是外部无法访问函数内部的变量,所以闭包可以理解成“定义在一个函数内部的函数,外部可以通过内部返回的函数访问内部函数的变量“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。 ## 如何判断 NaN ***** 参考答案:isNaN()方法 解析:isNaN(NaN) // true ## new 一个对象的过程中发生了什么 参考答案: ~~~js function Person(name) { this.name = name; } var person = new Person("qilei"); ~~~ new一个对象的四个过程: ~~~js // 1.创建空对象; var obj = {}; // 2.设置原型链: 设置新对象的 constructor 属性为构造函数的名称,设置新对象的__proto__属性指向构造函数的 prototype 对象; obj.constructor = Person; obj.__proto__ = Person.prototype; // 3.改变this指向:使用新对象调用函数,函数中的 this 指向新实例对象obj: var result = Person.call(obj); //{}.构造函数(); // 4.返回值:如果无返回值或者返回一个非对象值,则将新对象返回;如果返回值是一个新对象的话那么直接返回该对象。 if (typeof(result) == "object") { person = result; } else { person = obj; } ~~~ ## for in 和 for of的区别 **** 参考答案: 1、for in * 1.一般用于遍历对象的可枚举属性。以及对象从构造函数原型中继承的属性。对于每个不同的属性,语句都会被执行。 * 2.不建议使用 for in 遍历数组,因为输出的顺序是不固定的。 * 3.如果迭代的对象的变量值是 null 或者 undefined, for in 不执行循环体,建议在使用 for in 循环之前,先检查该对象的值是不是 null 或者 undefined。 2、for of * 1.for…of 语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。 解析: ~~~js var s = { a: 1, b: 2, c: 3 }; var s1 = Object.create(s); for (var prop in s1) { console.log(prop); //a b c console.log(s1[prop]); //1 2 3 } for (let prop of s1) { console.log(prop); //报错如下 Uncaught TypeError: s1 is not iterable } for (let prop of Object.keys(s1)) { console.log(prop); // a b c console.log(s1[prop]); //1 2 3 } ~~~ ## 如何判断 JS 变量的一个类型(至少三种方式)*** 参考答案:typeof、instanceof、 constructor、 prototype 解析: 1、typeof typeof 返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、object、undefined、function等6种数据类型。如果是判断一个基本的类型用typeof就是可以的。 ~~~js typeof ''; // string 有效 typeof 1; // number 有效 typeof true; //boolean 有效 typeof undefined; //undefined 有效 typeof null; //object 无效 typeof []; //object 无效 typeof new Function(); // function 有效 typeof new Date(); //object 无效 typeof new RegExp(); //object 无效 ~~~ 2、instanceof instanceof 是用来判断 A 是否为 B 的实例对,表达式为:A instanceof B,如果A是B的实例,则返回true, 否则返回false。 在这里需要特别注意的是:instanceof检测的是原型, ~~~js [] instanceof Array; //true {} instanceof Object; //true new Date() instanceof Date; //true ~~~ 3、constractor 每一个对象实例都可以通过 constrcutor 对象来访问它的构造函数 。JS 中内置了一些构造函数:Object、Array、Function、Date、RegExp、String等。我们可以通过数据的 constrcutor 是否与其构造函数相等来判断数据的类型。 ~~~js var arr = []; var obj = {}; var date = new Date(); var num = 110; var str = 'Hello'; var getName = function() {}; var sym = Symbol(); var set = new Set(); var map = new Map(); arr.constructor === Array; // true obj.constructor === Object; // true date.constructor === Date; // true str.constructor === String; // true getName.constructor === Function; // true sym.constructor === Symbol; // true set.constructor === Set; // true map.constructor === Map // true ~~~ 4、Object.prototype.toString toString是Object原型对象上的一个方法,该方法默认返回其调用者的具体类型,更严格的讲,是 toString运行时this指向的对象类型, 返回的类型格式为\[object, xxx\], xxx是具体的数据类型,其中包括:String, Number, Boolean, Undefined, Null, Function, Date, Array, RegExp, Error, HTMLDocument, ...基本上所有对象的类型都可以通过这个方法获取到。 ~~~js Object.prototype.toString.call(''); // [object String] Object.prototype.toString.call(1); // [object Number] Object.prototype.toString.call(true); // [object Boolean] Object.prototype.toString.call(undefined); // [object Undefined] Object.prototype.toString.call(null); // [object Null] Object.prototype.toString.call(new Function()); // [object Function] Object.prototype.toString.call(new Date()); // [object Date] Object.prototype.toString.call([]); // [object Array] Object.prototype.toString.call(new RegExp()); // [object RegExp] Object.prototype.toString.call(new Error()); // [object Error] ~~~ ## for in、Object.keys 和 Object.getOwnPropertyNames 对属性遍历有什么区别?**** 参考答案: * for in 会遍历自身及原型链上的可枚举属性 * Object.keys 会将对象自身的可枚举属性的 key 输出 * Object.getOwnPropertyNames会将自身所有的属性的 key 输出 解析: ECMAScript 将对象的属性分为两种:数据属性和访问器属性。 ~~~js var parent = Object.create(Object.prototype, { a: { value: 123, writable: true, enumerable: true, configurable: true } }); // parent继承自Object.prototype,有一个可枚举的属性a(enumerable:true)。 var child = Object.create(parent, { b: { value: 2, writable: true, enumerable: true, configurable: true }, c: { value: 3, writable: true, enumerable: false, configurable: true } }); //child 继承自 parent ,b可枚举,c不可枚举 ~~~ ### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#for-in)for in ~~~js for (var key in child) { console.log(key); } // b // a // for in 会遍历自身及原型链上的可枚举属性 ~~~ 如果只想输出自身的可枚举属性,可使用 hasOwnProperty 进行判断(数组与对象都可以,此处用数组做例子) ~~~js let arr = [1, 2, 3]; Array.prototype.xxx = 1231235; for (let i in arr) { if (arr.hasOwnProperty(i)) { console.log(arr[i]); } } // 1 // 2 // 3 ~~~ ### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#objectkeys)Object.keys ~~~js console.log(Object.keys(child)); // ["b"] // Object.keys 会将对象自身的可枚举属性的key输出 ~~~ ### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#objectgetownpropertynames)Object.getOwnPropertyNames ~~~js console.log(Object.getOwnPropertyNames(child)); // ["b","c"] // 会将自身所有的属性的key输出 ~~~ ## iframe 跨域通信和不跨域通信 参考答案: ### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#%E4%B8%8D%E8%B7%A8%E5%9F%9F%E9%80%9A%E4%BF%A1)不跨域通信 主页面 ~~~html <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title></title> </head> <body> <iframe name="myIframe" id="iframe" class="" src="flexible.html" width="500px" height="500px"> </iframe> </body> <script type="text/javascript" charset="utf-8"> function fullscreen() { alert(1111); } </script> </html> ~~~ 子页面 flexible.html ~~~html <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title></title> </head> <body> 我是子页面 </body> <script type="text/javascript" charset="utf-8"> // window.parent.fullScreens() function showalert() { alert(222); } </script> </html> ~~~ 1、主页面要是想要调取子页面的 showalert 方法 ~~~js myIframe.window.showalert(); ~~~ 2、子页面要掉主页面的 fullscreen 方法 ~~~js window.parent.fullScreens(); ~~~ 3、js 在 iframe 子页面获取父页面元素: ~~~js window.parent.document.getElementById("元素id"); ~~~ 4、js 在父页面获取 iframe 子页面元素代码如下: ~~~js window.frames["iframe_ID"].document.getElementById("元素id"); ~~~ ### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#%E8%B7%A8%E5%9F%9F%E9%80%9A%E4%BF%A1)跨域通信 使用[postMessage(官方用法)](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage) 子页面 ~~~js window.parent.postMessage("hello", "http://127.0.0.1:8089"); ~~~ 父页面接收 ~~~js window.addEventListener("message", function(event) { alert(123); }); ~~~ 解析:[参考](https://blog.csdn.net/weixin_41229588/article/details/93719894) ## 如何判断一个对象是否为数组 *** 参考答案: 第一种方法:使用 instanceof 操作符。 第二种方法:使用 ECMAScript 5 新增的 Array.isArray()方法。 第三种方法:使用使用 Object.prototype 上的原生 toString()方法判断。 ## script 标签的 defer 和 asnyc 属性的作用以及二者的区别? 参考答案: * 1、defer 和 async 的网络加载过程是一致的,都是异步执行。 * 2、区别在于加载完成之后什么时候执行,可以看出 defer 是文档所有元素解析完成之后才执行的。 * 3、如果存在多个 defer 脚本,那么它们是按照顺序执行脚本的,而 async,无论声明顺序如何,只要加载完成就立刻执行 解析: 无论`<script>`标签是嵌入代码还是引用外部文件,只要不包含 defer 属性和 async 属性(这两个属性只对外部文件有效),浏览器会按照`<script>`的出现顺序对他们依次进行解析,也就是说,只有在第一个`<script>`中的代码执行完成之后,浏览器才会执行第二个`<script>`中的代码,并且在解析时,页面的处理会暂时停止。 嵌入代码的解析=执行 外部文件的解析=下载+执行 script 标签存在两个属性,defer 和 async,这两个属性只对外部文件有效 ## [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#%E5%8F%AA%E6%9C%89%E4%B8%80%E4%B8%AA%E8%84%9A%E6%9C%AC%E7%9A%84%E6%83%85%E5%86%B5)只有一个脚本的情况 ~~~ <script src = "a.js" /> ~~~ 没有 defer 或 async 属性,浏览器会立即下载并执行相应的脚本,并且在下载和执行时页面的处理会停止。 ~~~ <script defer src = "a.js" /> ~~~ 有了 defer 属性,浏览器会立即下载相应的脚本,在下载的过程中页面的处理不会停止,等到文档解析完成脚本才会执行。 ~~~ <script async src = "a.js" /> ~~~ 有了 async 属性,浏览器会立即下载相应的脚本,在下载的过程中页面的处理不会停止,下载完成后立即执行,执行过程中页面处理会停止。 ~~~ <script defer async src = "a.js" /> ~~~ 如果同时指定了两个属性, 则会遵从 async 属性而忽略 defer 属性。 下图可以直观的看出三者之间的区别: [![js_002](https://github.com/yisainan/web-interview/raw/master/images/js_002.png)](https://github.com/yisainan/web-interview/blob/master/images/js_002.png) 其中蓝色代表 js 脚本网络下载时间,红色代表 js 脚本执行,绿色代表 html 解析。 ## [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#%E5%A4%9A%E4%B8%AA%E8%84%9A%E6%9C%AC%E7%9A%84%E6%83%85%E5%86%B5)多个脚本的情况 这里只列举两个脚本的情况: ~~~ <script src = "a.js"> </script> <script src = "b.js"> </script> ~~~ 没有 defer 或 async 属性,浏览器会立即下载并执行脚本 a.js,在 a.js 脚本执行完成后才会下载并执行脚本 b.js,在脚本下载和执行时页面的处理会停止。 ~~~ <script defer src = "a.js"> </script> <script defer src = "b.js"> </script> ~~~ 有了 defer 属性,浏览器会立即下载相应的脚本 a.js 和 b.js,在下载的过程中页面的处理不会停止,等到文档解析完成才会执行这两个脚本。HTML5 规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于 DOMContentLoaded 事件执行。 在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在 DOMContentLoaded 事件触发前执行,因此最好只包含一个延迟脚本。 ~~~ <script async src = "a.js"> </script> <script async src = "b.js"> </script> ~~~ 有了 async 属性,浏览器会立即下载相应的脚本 a.js 和 b.js,在下载的过程中页面的处理不会停止,a.js 和 b.js 哪个先下载完成哪个就立即执行,执行过程中页面处理会停止,但是其他脚本的下载不会停止。标记为 async 的脚本并不保证按照制定它们的先后顺序执行。异步脚本一定会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 事件触发之前或之后执行。 [参考](https://blog.csdn.net/weixin_42561383/article/details/86564715) ## Object.prototype.toString.call() 和 instanceOf 和 Array.isArray() 区别好坏 *** 参考答案: * Object.prototype.toString.call() * 优点:这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。 * 缺点:不能精准判断自定义对象,对于自定义对象只会返回\[object Object\] * instanceOf * 优点:instanceof 可以弥补 Object.prototype.toString.call()不能判断自定义实例化对象的缺点。 * 缺点: instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true,且不同于其他两种方法的是它不能检测出 iframes。 * Array.isArray() * 优点:当检测 Array 实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes * 缺点:只能判别数组 解析: ### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#objectprototypetostringcall)Object.prototype.toString.call() 每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 \[Object type\],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用 call 或者 apply 方法来改变 toString 方法的执行上下文。 ~~~js const an = ["Hello", "An"]; an.toString(); // "Hello,An" Object.prototype.toString.call(an); // "[object Array]" ~~~ 这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。 ~~~js Object.prototype.toString.call("An"); // "[object String]" Object.prototype.toString.call(1); // "[object Number]" Object.prototype.toString.call(Symbol(1)); // "[object Symbol]" Object.prototype.toString.call(null); // "[object Null]" Object.prototype.toString.call(undefined); // "[object Undefined]" Object.prototype.toString.call(function() {}); // "[object Function]" Object.prototype.toString.call({ name: "An" }); // "[object Object]" ~~~ 缺点:不能精准判断自定义对象,对于自定义对象只会返回\[object Object\] ~~~js function f(name) { this.name = name; } var f1 = new f("martin"); console.log(Object.prototype.toString.call(f1)); //[object Object] Object.prototype.toString.call(); // 常用于判断浏览器内置对象。 ~~~ ### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#instanceof)instanceof instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。 使用 instanceof 判断一个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false。 ~~~js [] instanceof Array; // true ~~~ 但 instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。 ~~~js [] instanceof Object; // true ~~~ 优点:instanceof 可以弥补 Object.prototype.toString.call()不能判断自定义实例化对象的缺点。 缺点:instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true,且不同于其他两种方法的是它不能检测出 iframes。 ~~~js function f(name) { this.name = name; } var f1 = new f("martin"); console.log(f1 instanceof f); //true ~~~ ### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#arrayisarray)Array.isArray() * 功能:用来判断对象是否为数组 * instanceof 与 isArray 当检测 Array 实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes ~~~js var iframe = document.createElement("iframe"); document.body.appendChild(iframe); xArray = window.frames[window.frames.length - 1].Array; var arr = new xArray(1, 2, 3); // [1,2,3] // Correctly checking for Array Array.isArray(arr); // true Object.prototype.toString.call(arr); // true // Considered harmful, because doesn't work though iframes arr instanceof Array; // false ~~~ 缺点:只能判别数组 * Array.isArray() 与 Object.prototype.toString.call() Array.isArray()是 ES5 新增的方法,当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。 ~~~js if (!Array.isArray) { Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === "[object Array]"; }; } ~~~ [参考](https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/23) ## JS 严格模式和正常模式 *** 这个写的太详细了, 刚学前端会说个作用就可以 参考答案:严格模式使用"use strict"; 作用: * 消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为; * 消除代码运行的一些不安全之处,保证代码运行的安全; * 提高编译器效率,增加运行速度; * 为未来新版本的 Javascript 做好铺垫。 表现: * 严格模式下, delete 运算符后跟随非法标识符(即 delete 不存在的标识符),会抛出语法错误; 非严格模式下,会静默失败并返回 false * 严格模式中,对象直接量中定义同名属性会抛出语法错误; 非严格模式不会报错 * 严格模式中,函数形参存在同名的,抛出错误; 非严格模式不会 * 严格模式不允许八进制整数直接量(如:023) * 严格模式中,arguments 对象是传入函数内实参列表的静态副本;非严格模式下,arguments 对象里的元素和对应的实参是指向同一个值的引用 * 严格模式中 eval 和 arguments 当做关键字,它们不能被赋值和用作变量声明 * 严格模式会限制对调用栈的检测能力,访问 arguments.callee.caller 会抛出异常 * 严格模式 变量必须先声明,直接给变量赋值,不会隐式创建全局变量,不能用 with, * 严格模式中 call apply 传入 null undefined 保持原样不被转换为 window 解析: 一、概述 除了正常运行模式,ECMAscript 5 添加了第二种运行模式:"严格模式"(strict mode)。顾名思义,这种模式使得 Javascript 在更严格的条件下运行。 设立"严格模式"的目的,主要有以下几个: * 消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为; * 消除代码运行的一些不安全之处,保证代码运行的安全; * 提高编译器效率,增加运行速度; * 为未来新版本的 Javascript 做好铺垫。 "严格模式"体现了 Javascript 更合理、更安全、更严谨的发展方向,包括 IE 10 在内的主流浏览器,都已经支持它,许多大项目已经开始全面拥抱它。 另一方面,同样的代码,在"严格模式"中,可能会有不一样的运行结果;一些在"正常模式"下可以运行的语句,在"严格模式"下将不能运行。掌握这些内容,有助于更细致深入地理解 Javascript,让你变成一个更好的程序员。 本文将对"严格模式"做详细介绍。 二、进入标志 进入"严格模式"的标志,是下面这行语句: "use strict"; 老版本的浏览器会把它当作一行普通字符串,加以忽略。 三、如何调用 "严格模式"有两种调用方法,适用于不同的场合。 3.1 针对整个脚本文件 将"use strict"放在脚本文件的第一行,则整个脚本都将以"严格模式"运行。如果这行语句不在第一行,则无效,整个脚本以"正常模式"运行。如果不同模式的代码文件合并成一个文件,这一点需要特别注意。 (严格地说,只要前面不是产生实际运行结果的语句,"use strict"可以不在第一行,比如直接跟在一个空的分号后面。) ~~~   <script>     "use strict";     console.log("这是严格模式。");   </script>   <script>     console.log("这是正常模式。");kly, it's almost 2 years ago now.I can admit it now - I run it on my school's network that has about 50 computers.   </script> ~~~ 上面的代码表示,一个网页中依次有两段 Javascript 代码。前一个 script 标签是严格模式,后一个不是。 3.2 针对单个函数 将"use strict"放在函数体的第一行,则整个函数以"严格模式"运行。 ~~~js function strict() { "use strict"; return "这是严格模式。"; } function notStrict() { return "这是正常模式。"; } ~~~ 3.3 脚本文件的变通写法 因为第一种调用方法不利于文件合并,所以更好的做法是,借用第二种方法,将整个脚本文件放在一个立即执行的匿名函数之中。 ~~~js (function() { "use strict"; // some code here })(); ~~~ 四、语法和行为改变 严格模式对 Javascript 的语法和行为,都做了一些改变。 4.1 全局变量显式声明 在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明。 ~~~js "use strict"; v = 1; // 报错,v未声明 for (i = 0; i < 2; i++) { // 报错,i未声明 } ~~~ 因此,严格模式下,变量都必须先用 var 命令声明,然后再使用。 4.2 静态绑定 Javascript 语言的一个特点,就是允许"动态绑定",即某些属性和方法到底属于哪一个对象,不是在编译时确定的,而是在运行时(runtime)确定的。 严格模式对动态绑定做了一些限制。某些情况下,只允许静态绑定。也就是说,属性和方法到底归属哪个对象,在编译阶段就确定。这样做有利于编译效率的提高,也使得代码更容易阅读,更少出现意外。 具体来说,涉及以下几个方面。 (1)禁止使用 with 语句 因为 with 语句无法在编译时就确定,属性到底归属哪个对象。 ~~~js "use strict"; var v = 1; with(o) { // 语法错误 v = 2; } ~~~ (2)创设 eval 作用域 正常模式下,Javascript 语言有两种变量作用域(scope):全局作用域和函数作用域。严格模式创设了第三种作用域:eval 作用域。 正常模式下,eval 语句的作用域,取决于它处于全局作用域,还是处于函数作用域。严格模式下,eval 语句本身就是一个作用域,不再能够生成全局变量了,它所生成的变量只能用于 eval 内部。 ~~~js "use strict"; var x = 2; console.info(eval("var x = 5; x")); // 5 console.info(x); // 2 ~~~ 4.3 增强的安全措施 (1)禁止 this 关键字指向全局对象 ~~~js function f() { return !this; } // 返回false,因为"this"指向全局对象,"!this"就是false function f() { "use strict"; return !this; } // 返回true,因为严格模式下,this的值为undefined,所以"!this"为true。 ~~~ 因此,使用构造函数时,如果忘了加 new,this 不再指向全局对象,而是报错。 ~~~js function f() { "use strict"; this.a = 1; } f(); // 报错,this未定义 ~~~ (2)禁止在函数内部遍历调用栈 ~~~js function f1() { "use strict"; f1.caller; // 报错 f1.arguments; // 报错 } f1(); ~~~ 4.4 禁止删除变量 严格模式下无法删除变量。只有 configurable 设置为 true 的对象属性,才能被删除。 ~~~js "use strict"; var x; delete x; // 语法错误 var o = Object.create(null, { 'x': { value: 1, configurable: true } }); delete o.x; // 删除成功 ~~~ 4.5 显式报错 正常模式下,对一个对象的只读属性进行赋值,不会报错,只会默默地失败。严格模式下,将报错。 ~~~js "use strict"; var o = {}; Object.defineProperty(o, "v", { value: 1, writable: false }); o.v = 2; // 报错 ~~~ 严格模式下,对一个使用 getter 方法读取的属性进行赋值,会报错。 ~~~js "use strict"; var o = { get v() { return 1; } }; o.v = 2; // 报错 ~~~ 严格模式下,对禁止扩展的对象添加新属性,会报错。 ~~~js "use strict"; var o = {}; Object.preventExtensions(o); o.v = 1; // 报错 ~~~ 严格模式下,删除一个不可删除的属性,会报错。 ~~~js "use strict"; delete Object.prototype; // 报错 ~~~ 4.6 重名错误 严格模式新增了一些语法错误。 (1)对象不能有重名的属性 正常模式下,如果对象有多个重名属性,最后赋值的那个属性会覆盖前面的值。严格模式下,这属于语法错误。 ~~~js "use strict"; var o = { p: 1, p: 2 }; // 语法错误 ~~~ (2)函数不能有重名的参数 正常模式下,如果函数有多个重名的参数,可以用 arguments\[i\]读取。严格模式下,这属于语法错误。 ~~~js "use strict"; function f(a, a, b) { // 语法错误 return; } ~~~ 4.7 禁止八进制表示法 正常模式下,整数的第一位如果是 0,表示这是八进制数,比如 0100 等于十进制的 64。严格模式禁止这种表示法,整数第一位为 0,将报错。 ~~~js "use strict"; var n = 0100; // 语法错误 ~~~ 4.8 arguments 对象的限制 arguments 是函数的参数对象,严格模式对它的使用做了限制。 (1)不允许对 arguments 赋值 ~~~js "use strict"; arguments++; // 语法错误 var obj = { set p(arguments) {} }; // 语法错误 try {} catch (arguments) {} // 语法错误 function arguments() {} // 语法错误 var f = new Function("arguments", "'use strict'; return 17;"); // 语法错误 ~~~ (2)arguments 不再追踪参数的变化 ~~~js function f(a) { a = 2; return [a, arguments[0]]; } f(1); // 正常模式为[2,2] function f(a) { "use strict"; a = 2; return [a, arguments[0]]; } f(1); // 严格模式为[2,1] ~~~ (3)禁止使用 arguments.callee 这意味着,你无法在匿名函数内部调用自身了。 ~~~js "use strict"; var f = function() { return arguments.callee; }; f(); // 报错 ~~~ 4.9 函数必须声明在顶层 将来 Javascript 的新版本会引入"块级作用域"。为了与新版本接轨,严格模式只允许在全局作用域或函数作用域的顶层声明函数。也就是说,不允许在非函数的代码块内声明函数。 ~~~js "use strict"; if (true) { function f() {} // 语法错误 } for (var i = 0; i < 5; i++) { function f2() {} // 语法错误 } ~~~ 4.10 保留字 为了向将来 Javascript 的新版本过渡,严格模式新增了一些保留字:implements, interface, let, package, private, protected, public, static, yield。 使用这些词作为变量名将会报错。 ~~~js function package(protected) { // 语法错误 "use strict"; var implements; // 语法错误 } ~~~ 此外,ECMAscript 第五版本身还规定了另一些保留字(class, enum, export, extends, import, super),以及各大浏览器自行增加的 const 保留字,也是不能作为变量名的。 [参考](https://www.ruanyifeng.com/blog/2013/01/javascript_strict_mode.html) ## JS 块级作用域、变量提升 ******* 参考答案: 1.块级作用域 JS 中作用域有:全局作用域、函数作用域。没有块作用域的概念。ECMAScript 6(简称 ES6)中新增了块级作用域。块作用域由 { } 包括,if 语句和 for 语句里面的{ }也属于块作用域。 2.变量提升 * 如果变量声明在函数里面,则将变量声明提升到函数的开头 * 如果变量声明是一个全局变量,则将变量声明提升到全局作用域的开头 解析: ~~~js < script type = "text/javascript" > { var a = 1; console.log(a); // 1 } console.log(a); // 1 // 可见,通过var定义的变量可以跨块作用域访问到。 (function A() { var b = 2; console.log(b); // 2 })(); // console.log(b); // 报错, // 可见,通过var定义的变量不能跨函数作用域访问到 if (true) { var c = 3; } console.log(c); // 3 for (var i = 0; i < 4; i++) { var d = 5; }; console.log(i); // 4 (循环结束i已经是4,所以此处i为4) console.log(d); // 5 // if语句和for语句中用var定义的变量可以在外面访问到, // 可见,if语句和for语句属于块作用域,不属于函数作用域。 { var a = 1; let b = 2; const c = 3; { console.log(a); // 1 子作用域可以访问到父作用域的变量 console.log(b); // 2 子作用域可以访问到父作用域的变量 console.log(c); // 3 子作用域可以访问到父作用域的变量 var aa = 11; let bb = 22; const cc = 33; } console.log(aa); // 11 // 可以跨块访问到子 块作用域 的变量 // console.log(bb); // 报错 bb is not defined // console.log(cc); // 报错 cc is not defined } < /script> ~~~ 拓展: ### var、let、const 的区别 ***** * var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。 * let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。 * const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。 * 同一个变量只能使用一种方式声明,不然会报错 ~~~js < script type = "text/javascript" > // 块作用域 { var a = 1; let b = 2; const c = 3; // c = 4; // 报错 // let a = 'a'; // 报错 注:是上面 var a = 1; 那行报错 // var b = 'b'; // 报错:本行报错 // const a = 'a1'; // 报错 注:是上面 var a = 1; 那行报错 // let c = 'c'; // 报错:本行报错 var aa; let bb; // const cc; // 报错 console.log(a); // 1 console.log(b); // 2 console.log(c); // 3 console.log(aa); // undefined console.log(bb); // undefined } console.log(a); // 1 // console.log(b); // 报错 // console.log(c); // 报错 // 函数作用域 (function A() { var d = 5; let e = 6; const f = 7; console.log(d); // 5 console.log(e); // 6 (在同一个{ }中,也属于同一个块,可以正常访问到) console.log(f); // 7 (在同一个{ }中,也属于同一个块,可以正常访问到) })(); // console.log(d); // 报错 // console.log(e); // 报错 // console.log(f); // 报错 < /script> ~~~ ## null/undefined 的区别 ***** 参考答案: null: Null 类型,代表“空值",代表一个空对象指针,使用 typeof 运算得到 “object",所以你可以认为它是一个特殊的对象值。 undefined: Undefined 类型,当一个声明了一个变量未初始化时,得到的就是 undefined。 ## 重排与重绘的区别,什么情况下会触发?*** 参考答案: 1.简述重排的概念 浏览器下载完页面中的所有组件(HTML、JavaScript、CSS、图片)之后会解析生成两个内部数据结构(DOM 树和渲染树),DOM 树表示页面结构,渲染树表示 DOM 节点如何显示。重排是 DOM 元素的几何属性变化,DOM 树的结构变化,渲染树需要重新计算。 2.简述重绘的概念 重绘是一个元素外观的改变所触发的浏览器行为,例如改变 visibility、outline、背景色等属性。浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。由于浏览器的流布局,对渲染树的计算通常只需要遍历一次就可以完成。但 table 及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性值,比同等元素要多花两倍时间,这就是我们尽量避免使用 table 布局页面的原因之一。 3.简述重绘和重排的关系 重绘不会引起重排,但重排一定会引起重绘,一个元素的重排通常会带来一系列的反应,甚至触发整个文档的重排和重绘,性能代价是高昂的。 4.什么情况下会触发重排? * 页面渲染初始化时;(这个无法避免) * 浏览器窗口改变尺寸; * 元素尺寸改变时; * 元素位置改变时; * 元素内容改变时; * 添加或删除可见的 DOM 元素时。 5.重排优化有如下五种方法 * 将多次改变样式属性的操作合并成一次操作,减少 DOM 访问。 * 如果要批量添加 DOM,可以先让元素脱离文档流,操作完后再带入文档流,这样只会触发一次重排。(fragment 元素的应用) * 将需要多次重排的元素,position 属性设为 absolute 或 fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。 * 由于 display 属性为 none 的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发两次重排。 * 在内存中多次操作节点,完成后再添加到文档中去。例如要异步获取表格数据,渲染到页面。可以先取得数据后在内存中构建整个表格的 html 片段,再一次性添加到文档中去,而不是循环添加每一行。 ## JavaScript 的数据类型 ***** 参考答案:JS 数据类型共有六种,分别是 String、Number、Boolean、Null、Undefined 和 Object 等, 另外,ES6 新增了 Symbol 类型。其中,Object 是引用类型,其他的都是基本类型(Primitive Type)。 ## 如何判断一个对象是否属于某个类***** 参考答案:instanceof 解析: ~~~js if (a instanceof Person) { alert("yes"); } ~~~ ## new 操作符具体干了什么呢? *** 参考答案: 样本一 new 共经过了 4 几个阶段 * 1、创建一个空对象 * 2、设置原型链 * 3、让 Func 中的 this 指向 obj,并执行 Func 的函数体 * 4、判断 Func 的返回值类型: 样本二 ~~~ function Test(){} const test = new Test() ~~~ 1.创建一个新对象: ~~~ const obj = {} ~~~ 2.设置新对象的 constructor 属性为构造函数的名称,设置新对象的\_\_proto\_\_属性指向构造函数的 prototype 对象 ~~~ obj.constructor = Test obj.__proto__ = Test.prototype ~~~ 3.使用新对象调用函数,函数中的 this 被指向新实例对象 ~~~ Test.call(obj) ~~~ 4.将初始化完毕的新对象地址,保存到等号左边的变量中 ## call() 和 apply() 的含义和区别? **** 参考答案: 首先说明两个方法的含义: * call:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.call(A, args1, args2); 即 A 对象调用 B 对象的方法。 * apply:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.apply(A, arguments); 即 A 对象应用 B 对象的方法。 call 与 apply 的相同点: * 方法的含义是一样的,即方法功能是一样的; * 第一个参数的作用是一样的; call 与 apply 的不同点:两者传入的列表形式不一样 * call 可以传入多个参数; * apply 只能传入两个参数,所以其第二个参数往往是作为数组形式传入 想一想哪个性能更好一些呢? ## 解释 JavaScript 中的作用域与变量声明提升?***** 参考答案: * 我对作用域的理解是只会对某个范围产生作用,而不会对外产生影响的封闭空间。在这样的一些空间里,外部不能访问内部变量,但内部可以访问外部变量。 * 所有申明都会被提升到作用域的最顶上 * 同一个变量申明只进行一次,并且因此其他申明都会被忽略 * 函数声明的优先级优于变量申明,且函数声明会连带定义一起被提升 ## bind、call、apply 的区别 *** 参考答案: call 和 apply 其实是一样的,区别就在于传参时参数是一个一个传或者是以一个数组的方式来传。 call 和 apply 都是在调用时生效,改变调用者的 this 指向。 let name = 'Jack' const obj = {name: 'Tom'} function sayHi() {console.log('Hi! ' + this.name)} sayHi() // Hi! Jack sayHi.call(obj) // Hi! Tom bind 也是改变 this 指向,不过不是在调用时生效,而是返回一个新函数。 const newFunc = sayHi.bind(obj) newFunc() // Hi! Tom ## 如何获取浏览器版本信息 *** window.navigator.userAgent ### 作用域的概念及作用 参考答案: * 作用域 : 起作用的一块区域 * 作用域的概念: 对变量起保护作用的一块区域 * 作用: 作用域外部无法获取到作用域内部声明的变量,作用域内部能够获取到作用域外界声明的变量。 ## 作用域的分类***** 参考答案:块作用域、词法作用域、动态作用域 解析: 1 块作用域 花括号 {} 2 词法作用域(js 属于词法作用域) 作用域只跟在何处被创建有关系,跟在何处被调用没有关系 3 动态作用域 作用域只跟在何处被调用有关系,跟在何处被创建没有关系 ## js 属于哪种作用域 *** 参考答案:词法作用域(函数作用域) 解析: ~~~js // 块作用域 /*{ var num =123; } console.log(num);*/ // 如果js属于块作用域,那么在花括号外部就无法访问到花括号内部的声明的num变量。 // 如果js不属于块级作用域,那么花括号外部就能够访问到花括号内部声明的num变量 // 能够输出num变量,也就说明js不属于块级作用。 // 在ES6 之前的版本js是不存在块级作用域的。 //js属于词法作用域还是动态作用域 // js中函数可以帮我们去形成一个作用域 /* function fn(){ var num =123; } fn(); //在函数外界能否访问到num这样一个变量 console.log(num)*/ //Uncaught ReferenceError: num is not defined // 如果函数能够生成一个作用域,那么在函数外界就无法访问到函数内部声明的变量。 // js中的函数能够生成一个作用。 函数作用域 。 // 词法作用域:作用的外界只跟作用域在何处创建有关系,跟作用域在何处被调用没有关系 var num = 123; function f1() { console.log(num); // } function f2() { var num = 456; f1(); //f1在f2被调用的时候会被执行 。 } f2(); //如果js是词法作用域,那么就会输出f1被创建的时候外部的num变量 123 //如果js是动态作用域,那么f1执行的时候就会输出f1被调用时外部环境中的num 456 ~~~ ## 自执行函数? 用于什么场景?好处?*** 参考答案: 自执行函数: 1、声明一个匿名函数 2、马上调用这个匿名函数。 作用:创建一个独立的作用域。 好处:防止变量弥散到全局,以免各种 js 库冲突。隔离作用域避免污染,或者截断作用域链,避免闭包造成引用变量无法释放。利用立即执行特性,返回需要的业务函数或对象,避免每次通过条件判断来处理 场景:一般用于框架、插件等场景 ## 多个页面之间如何进行通信***** 参考答案:有如下几个方式: * cookie * web worker * localeStorage 和 sessionStorage ## 如何做到修改 url 参数页面不刷新*** 参考答案: HTML5 引入了`history.pushState()`和`history.replaceState()`方法,它们分别可以添加和修改历史记录条目。 ~~~js let stateObj = { foo: "bar" }; history.pushState(stateObj, "page 2", "bar.html"); ~~~ 假设当前页面为`foo.html`,执行上述代码后会变为`bar.html`,点击浏览器后退,会变为`foo.html`,但浏览器并不会刷新。`pushState()`需要三个参数: 一个状态对象, 一个标题 (目前被忽略), 和 (可选的) 一个 URL.让我们来解释下这三个参数详细内容: * 状态对象 — 状态对象`state`是一个 JavaScript 对象,通过`pushState ()`创建新的历史记录条目。无论什么时候用户导航到新的状态,`popstate`事件就会被触发,且该事件的`state`属性包含该历史记录条目状态对象的副本。 状态对象可以是能被序列化的任何东西。原因在于 Firefox 将状态对象保存在用户的磁盘上,以便在用户重启浏览器时使用,我们规定了状态对象在序列化表示后有 640k 的大小限制。如果你给`pushState()`方法传了一个序列化后大于 640k 的状态对象,该方法会抛出异常。如果你需要更大的空间,建议使用`sessionStorage`以及`localStorage`. * 标题 — Firefox 目前忽略这个参数,但未来可能会用到。传递一个空字符串在这里是安全的,而在将来这是不安全的。二选一的话,你可以为跳转的`state`传递一个短标题。 * URL — 该参数定义了新的历史 URL 记录。注意,调用`pushState()`后浏览器并不会立即加载这个 URL,但可能会在稍后某些情况下加载这个 URL,比如在用户重新打开浏览器时。新 URL 不必须为绝对路径。如果新 URL 是相对路径,那么它将被作为相对于当前 URL 处理。新 URL 必须与当前 URL 同源,否则`pushState()`会抛出一个异常。该参数是可选的,缺省为当前 URL。 ## 数组方法 pop() push() unshift() shift()**** 参考答案: * arr.pop() 从后面删除元素,只能是一个,返回值是删除的元素 * arr.push() 从后面添加元素,返回值为添加完后的数组的长度 * arr.unshift() 从前面添加元素, 返回值是添加完后的数组的长度 * arr.shift() 从前面删除元素,只能删除一个 返回值是删除的元素 ## 事件绑定与普通事件有什么区别*** 参考答案: * 用普通事件添加相同事件,下面会覆盖上面的,而事件绑定不会 * 普通事件是针对非 dom 元素,事件绑定是针对 dom 元素的事件 ## 变量提升***** 参考答案: ### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#%E5%8F%98%E9%87%8F%E6%8F%90%E5%8D%87)变量提升 A、js 代码执行的过程 * 1 变量提升 * 2 代码从上到下依次执行 var 关键字和 function 关键字声明的变量会进行变量提升 B、变量提升发生的环境:发生在代码所处的当前作用域。 * 变量提升 * 1 var 关键字进行的变量提升,会把变量提前声明,但是不会提前赋值 。 * 2 function 关键字对变量进行变量提升,既会把变量提前声明,又会把变量提前赋值,也就是把整个函数体提升到代码的顶部 * 3 有一些代码是不会执行的但是仍旧会发生变量提升, 规则适用于 1, 2 * 3.1 return 之后的代码依旧会发生变量提升,规则适用于 1,2 * 3.2 代码报错之后的代码依旧会发生变量提升,规则适用于 1,2 * 3.3 break 之后的代码依旧会发生变量提升,规则适用于 1, 2 * 4 有一些代码是不会执行但是仍旧会发生变量提升,但是规则要发生变化 * 4.1 if 判断语句 if 判断语句中 var 关键字以及 function 关键字声明的变量只会发生提前声明,不会发生提前赋值, 也就是不会吧函数体整体提升到当前作用域顶部。规则跟 1, 2 不适用 * 4.2 switch case 规则跟 1, 2 不适用 * 4.3 do while 规则跟 1, 2 不适用 * 4.4 try catch catch 中声明的变量只会发生提前声明,不会发生提前赋值。 * Ps: 在条件判断语句和 try catch 中的声明的变量不管是否能够执行,都只会发生提前 * 声明,不会发生提前赋值。 解析: ~~~js // 如果一个变量声明了但是未赋值,那么输出这个变量就会输出 undefined var num; console.log(num); // 如果一个变量没有声明也没有赋值,那么就会报一个错: console.log(num); // 输出一个不存在的变量 Uncaught ReferenceError: num is not defined ~~~ ~~~js // var 关键字进行的变量提升 console.log(num); var num = 123; console.log(num); var num = 456; console.log(num); // 变量提升之后的代码: var num; console.log(num); num = 123; console.log(num); num = 456; console.log(num); ~~~ ~~~js // function 关键字的变量提升 console.log(fn); function fn() { console.log(1); } // 变量提升之后的代码: function fn() { console.log(1); } console.log(fn); // 输出fn的函数体 ~~~ ~~~js // 3.1 return 之后的代码依旧会发生变量提升 规则适用于1,2 function fn() { console.log(num); return; var num = 123; } fn(); // 变量提升之后的代码: function fn() { var num; console.log(num); return; num = 123; } fn(); // undefined function fn() { console.log(fo); return; function fo() {} } fn(); // 变量提升之后的代码: function fn() { function fo() {} console.log(fo); return; } fn(); //输出fo的函数体 ~~~ ~~~js //3.2 代码报错之后的代码依旧会进行变量提升,规则适用于1,2 console.log(num); xsasfgdsfqdfsdf; //报一个错 var num = 123; console.log(num); // 变量提升之后的代码: var num; console.log(num); //输出 undefined dsagdsqghdwfh; // 报一个错误 ,错误之后的代码不会被执行 num = 123; console.log(num); ~~~ ~~~js //function 关键字 console.log(fn); sasgfdhwhsdqg; function fn() {} console.log(fn); // 变量提升之后的代码: function fn() {} console.log(fn); // 输出 fn 的函数体 asdgsdgdfgfdg; // 报一个错误,报错之后的代码不会被执行 console.log(fn); ~~~ ~~~js //4 代码不执行,但是会进行变量提升,不过规则不适用于1,2 //4.1 if判断语句 console.log(num); if (false) { var num = 123; } console.log(num) // 变量提升之后的代码: var num; console.log(num); //undefined if (false) { num = 123; } console.log(num) //undefined console.log(fn); if (false) { function fn() {} } console.log(fn); // 变量提升之后的代码: var fn; function fn; console.log(fn) //undefined if (false) { function fn() {} } console.log(fn) //undefined /*function fn//Uncaught SyntaxError: Unexpected end of input*/ ~~~ ~~~js // try catch try { console.log(num); } catch (e) { var num = 123; } console.log(num); var num; try { console.log(num); // undefined } catch (e) { num = 123; } console.log(num); // undefined try { console.log(fn); } catch (e) { function fn() {} } console.log(fn); var fn; try { console.log(fn); // undefined } catch (e) { num = 123; } console.log(fn); // undefined ~~~ [对应面试题](https://github.com/yisainan/web-interview/blob/master/content/%E7%BC%96%E7%A8%8B%E9%A2%98/%E5%8F%98%E9%87%8F%E6%8F%90%E5%8D%87.md) ## 如何阻止冒泡与默认行为 参考答案: * 阻止冒泡行为:非 IE 浏览器 stopPropagation(),IE 浏览器 window.event.cancelBubble = true * 阻止默认行为:非 IE 浏览器 preventDefault(),IE 浏览器 window.event.returnValue = false 解析: 当需要阻止冒泡行为时,可以使用 ~~~js function stopBubble(e) { //如果提供了事件对象,则这是一个非IE浏览器 if (e && e.stopPropagation) //因此它支持W3C的stopPropagation()方法 e.stopPropagation(); //否则,我们需要使用IE的方式来取消事件冒泡 else window.event.cancelBubble = true; } ~~~ 当需要阻止默认行为时,可以使用 ~~~js //阻止浏览器的默认行为 function stopDefault(e) { //阻止默认浏览器动作(W3C) if (e && e.preventDefault) e.preventDefault(); //IE中阻止函数器默认动作的方式 else window.event.returnValue = false; return false; } ~~~ ## js 中 this 闭包 作用域 ***** 参考答案: this:指向调用上下文 闭包:定义一个函数就开辟了一个局部作用域,整个 js 执行环境有一个全局作用域 作用域:一个函数可以访问其他函数中的变量(闭包是一个受保护的变量空间) ~~~js var f = (function fn() { var name = 1; return function() { name++; console.log(name) } })() == > undefined 有疑问 ~~~ ## javascript 的同源策略 ***** 参考答案:一段脚本只能读取来自于同一来源的窗口和文档的属性 解析: 同源策略:限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。(来自 MDN 官方的解释) 简单来说就是:一段脚本只能读取来自于同一来源的窗口和文档的属性,这里的同一来源指的是主机名、协议和端口号的组合 具体解释: (1)源包括三个部分:协议、域名、端口(http 协议的默认端口是 80)。如果有任何一个部分不同,则源不同,那就是跨域了。 (2)限制:这个源的文档没有权利去操作另一个源的文档。这个限制体现在:(要记住) Cookie、LocalStorage 和 IndexDB 无法获取。 无法获取和操作 DOM。 不能发送 Ajax 请求。我们要注意,Ajax 只适合同源的通信。 同源策略带来的麻烦:ajax 在不同域名下的请求无法实现,需要进行跨域操作 ## 事件冒泡与事件捕获 *** 参考答案: 事件冒泡:由最具体的元素(目标元素)向外传播到最不具体的元素 事件捕获:由最不确定的元素到目标元素 ## foo = foo||bar ,这行代码是什么意思?为什么要这样写? ***** && 如果两个操作数都非零,则条件为真; || 如果两个操作数中有任意一个非零,则条件为真。 [## | 和 ||,& 和 && 的区别](https://www.runoob.com/note/34429) 参考答案: 这种写法称为短路表达式 解析: 相当于 var foo; if (foo) { foo = foo; } else { foo = bar; } ## javascript 中 this 的指向问题******* 前端 this 指向问题很重要 参考答案: * 全局环境、普通函数(非严格模式)指向 window * 普通函数(严格模式)指向 undefined * 函数作为对象方法及原型链指向的就是上一级的对象 * 构造函数指向构造的对象 * DOM 事件中指向触发事件的元素 * 箭头函数... 解析: ## [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#1%E5%85%A8%E5%B1%80%E7%8E%AF%E5%A2%83)1、全局环境 全局环境下,this 始终指向全局对象(window),无论是否严格模式; ~~~js // 在浏览器中,全局对象为 window 对象: console.log(this === window); // true this.a = 37; console.log(window.a); // 37 ~~~ ## [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#2%E5%87%BD%E6%95%B0%E4%B8%8A%E4%B8%8B%E6%96%87%E8%B0%83%E7%94%A8)2、函数上下文调用 2.1 普通函数 普通函数内部的 this 分两种情况,严格模式和非严格模式。 (1)非严格模式下,没有被上一级的对象所调用, this 默认指向全局对象 window。 ~~~js function f1() { return this; } f1() === window; // true ~~~ (2)严格模式下,this 指向 undefined。 ~~~js function f2() { "use strict"; // 这里是严格模式 return this; } f2() === undefined; // true ~~~ 2.2 函数作为对象的方法 (1)函数有被上一级的对象所调用,那么 this 指向的就是上一级的对象。 (2)多层嵌套的对象,内部方法的 this 指向离被调用函数最近的对象(window 也是对象,其内部对象调用方法的 this 指向内部对象, 而非 window)。 ~~~js //方式1 var o = { prop: 37, f: function() { return this.prop; } }; //当 o.f()被调用时,函数内的this将绑定到o对象。 console.log(o.f()); // logs 37 //方式2 var o = { prop: 37 }; function independent() { return this.prop; } //函数f作为o的成员方法调用 o.f = independent; console.log(o.f()); // logs 37 //方式3 //this 的绑定只受最靠近的成员引用的影响 o.b = { g: independent, prop: 42 }; console.log(o.b.g()); // 42 ~~~ 特殊例子 ~~~js // 例子1 var o = { a: 10, b: { // a:12, fn: function() { console.log(this.a); //undefined console.log(this); //{fn: ƒ} } } }; o.b.fn(); // 例子2 var o = { a: 10, b: { a: 12, fn: function() { console.log(this.a); //undefined console.log(this); //window } } }; var j = o.b.fn; j(); // this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的,例子2中虽然函数fn是被对象b所引用,但是在将fn赋值给变量j的时候并没有执行所以最终指向的是window,这和例子1是不一样的,例子1是直接执行了fn ~~~ 2.3 原型链中的 this (1)如果该方法存在于一个对象的原型链上,那么 this 指向的是调用这个方法的对象,就像该方法在对象上一样。 ~~~js var o = { f: function() { return this.a + this.b; } }; var p = Object.create(o); p.a = 1; p.b = 4; console.log(p.f()); // 5 ~~~ 上述例子中,对象 p 没有属于它自己的 f 属性,它的 f 属性继承自它的原型。当执行 p.f()时,会查找 p 的原型链,找到 f 函数并执行。因为 f 是作为 p 的方法调用的,所以函数中的 this 指向 p。 (2)相同的概念也适用于当函数在一个 getter 或者 setter 中被调用。用作 getter 或 setter 的函数都会把 this 绑定到设置或获取属性的对象。 (3)call()和 apply()方法:当函数通过 Function 对象的原型中继承的方法 call() 和 apply() 方法调用时, 其函数内部的 this 值可绑定到 call() & apply() 方法指定的第一个对象上, 如果第一个参数不是对象,JavaScript 内部会尝试将其转换成对象然后指向它。 ~~~js function add(c, d) { return 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 function tt() { console.log(this); } // 第一个参数不是对象,JavaScript内部会尝试将其转换成对象然后指向它。 tt.call(5); // 内部转成 Number {[[PrimitiveValue]]: 5} tt.call("asd"); // 内部转成 String {0: "a", 1: "s", 2: "d", length: 3, [[PrimitiveValue]]: "asd"} ~~~ (4)bind()方法:由 ES5 引入, 在 Function 的原型链上, Function.prototype.bind。通过 bind 方法绑定后, 函数将被永远绑定在其第一个参数对象上, 而无论其在什么情况下被调用。 ~~~js function f() { return this.a; } var g = f.bind({ a: "azerty" }); console.log(g()); // azerty var o = { a: 37, f: f, g: g }; console.log(o.f(), o.g()); // 37, azerty ~~~ 2.4 构造函数中的 this 当一个函数用作构造函数时(使用 new 关键字),它的 this 被绑定到正在构造的新对象。 构造器返回的默认值是 this 所指的那个对象,也可以手动返回其他的对象。 ~~~js function C() { this.a = 37; } var o = new C(); console.log(o.a); // 37 // 为什么this会指向o?首先new关键字会创建一个空的对象,然后会自动调用一个函数apply方法,将this指向这个空对象,这样的话函数内部的this就会被这个空的对象替代。 function C2() { this.a = 37; return { a: 38 }; // 手动设置返回{a:38}对象 } o = new C2(); console.log(o.a); // 38 ~~~ 特殊例子 当 this 碰到 return 时 ~~~js // 例子1 function fn() { this.user = "追梦子"; return {}; } var a = new fn(); console.log(a.user); //undefined // 例子2 function fn() { this.user = "追梦子"; return function() {}; } var a = new fn(); console.log(a.user); //undefined // 例子3 function fn() { this.user = "追梦子"; return 1; } var a = new fn(); console.log(a.user); //追梦子 // 例子4 function fn() { this.user = "追梦子"; return undefined; } var a = new fn(); console.log(a.user); //追梦子 // 例子5 function fn() { this.user = "追梦子"; return undefined; } var a = new fn(); console.log(a); //fn {user: "追梦子"} // 例子6 // 虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊 function fn() { this.user = "追梦子"; return null; } var a = new fn(); console.log(a.user); //追梦子 // 总结:如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。 ~~~ 2.5 setTimeout & setInterval (1)对于延时函数内部的回调函数的 this 指向全局对象 window; (2)可以通过 bind()方法改变内部函数 this 指向。 ~~~js //默认情况下代码 function Person() { this.age = 0; setTimeout(function() { console.log(this); }, 3000); } var p = new Person(); //3秒后返回 window 对象 //通过bind绑定 function Person() { this.age = 0; setTimeout( function() { console.log(this); }.bind(this), 3000 ); } var p = new Person(); //3秒后返回构造函数新生成的对象 Person{...} ~~~ ## [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#3%E5%9C%A8-dom-%E4%BA%8B%E4%BB%B6%E4%B8%AD)3、在 DOM 事件中 3.1 作为一个 DOM 事件处理函数 当函数被用作事件处理函数时,它的 this 指向触发事件的元素(针对 addEventListener 事件)。 ~~~js // 被调用时,将关联的元素变成蓝色 function bluify(e) { //this指向所点击元素 console.log("this === e.currentTarget", this === e.currentTarget); // 总是 true // 当 currentTarget 和 target 是同一个对象时为 true console.log("this === e.target", this === e.target); this.style.backgroundColor = "#A5D9F3"; } // 获取文档中的所有元素的列表 var elements = document.getElementsByTagName("*"); // 将bluify作为元素的点击监听函数,当元素被点击时,就会变成蓝色 for (var i = 0; i < elements.length; i++) { elements[i].addEventListener("click", bluify, false); } ~~~ 3.2 作为一个内联事件处理函数 (1)当代码被内联处理函数调用时,它的 this 指向监听器所在的 DOM 元素; (2)当代码被包括在函数内部执行时,其 this 指向等同于 普通函数直接调用的情况,即在非严格模式指向全局对象 window,在严格模式指向 undefined: ~~~html <button onclick="console.log(this)">show me</button> <button onclick="(function () {console.log(this)})()">show inner this</button> <button onclick="(function () {'use strict'; console.log(this)})()"> use strict </button> ~~~ ~~~ // 控制台打印 <button onclick="console.log(this)">show me</button> Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …} undefined ~~~ ## [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#4%E7%AE%AD%E5%A4%B4%E5%87%BD%E6%95%B0)4、箭头函数 4.1 全局环境中 在全局代码中,箭头函数被设置为全局对象: ~~~js var globalObject = this; var foo = () => this; console.log(foo() === globalObject); // true ~~~ 4.2 this 捕获上下文 箭头函数没有自己的 this,而是使用箭头函数所在的作用域的 this,即指向箭头函数定义时(而不是运行时)所在的作用域。 ~~~js //1、箭头函数在函数内部,以非方法的方法使用 function Person() { this.age = 0; setInterval(() => { this.age++; }, 3000); } var p = new Person(); //Person{age: 0} //普通函数作为内部函数 function Person() { this.age = 0; setInterval(function() { console.log(this); this.age++; }, 3000); } var p = new Person(); //Window{...} ~~~ 4.2 this 捕获上下文 箭头函数没有自己的 this,而是使用箭头函数所在的作用域的 this,即指向箭头函数定义时(而不是运行时)所在的作用域。 ~~~js //1、箭头函数在函数内部,以非方法的方法使用 function Person() { this.age = 0; setInterval(() => { console.log(this); this.age++; }, 3000); } var p = new Person(); //Person{age: 0} //普通函数作为内部函数 function Person() { this.age = 0; setInterval(function() { console.log(this); this.age++; }, 3000); } var p = new Person(); //Window{...} ~~~ 在 setTimeout 中的 this 指向了构造函数新生成的对象,而普通函数指向了全局 window 对象。 4.3 箭头函数作为对象的方法使用 箭头函数作为对象的方法使用,指向全局 window 对象;而普通函数作为对象的方法使用,则指向调用的对象。 ~~~js var obj = { i: 10, b: () => console.log(this.i, this), c: function() { console.log(this.i, this); } }; obj.b(); // undefined window{...} obj.c(); // 10 Object {...} ~~~ 4.4 箭头函数中,call()、apply()、bind()方法无效 ~~~js var adder = { base: 1, //对象的方法内部定义箭头函数,this是箭头函数所在的作用域的this, //而方法add的this指向adder对象,所以箭头函数的this也指向adder对象。 add: function(a) { var f = v => v + this.base; return f(a); }, //普通函数f1的this指向window add1: function() { var f1 = function() { console.log(this); }; return f1(); }, addThruCall: function inFun(a) { var f = v => v + this.base; var b = { base: 2 }; return f.call(b, a); } }; console.log(adder.add(1)); // 输出 2 adder.add1(); //输出全局对象 window{...} console.log(adder.addThruCall(1)); // 仍然输出 2(而不是3,其内部的this并没有因为call() 而改变,其this值仍然为函数inFun的this值,指向对象adder ~~~ 4.5 this 指向固定化 箭头函数可以让 this 指向固定化,这种特性很有利于封装回调函数 ~~~js var handler = { id: "123456", init: function() { document.addEventListener( "click", event => this.doSomething(event.type), false ); }, doSomething: function(type) { console.log("Handling " + type + " for " + this.id); } }; ~~~ 上面代码的 init 方法中,使用了箭头函数,这导致这个箭头函数里面的 this,总是指向 handler 对象。如果不使用箭头函数则指向全局 document 对象。 4.6 箭头函是不适用场景 (1)箭头函数不适合定义对象的方法(方法内有 this),因为此时指向 window; (2)需要动态 this 的时候,也不应使用箭头函数。 ~~~js //例1,this指向定义箭头函数所在的作用域,它位于对象cat内,但cat不能构成一个作用域,所以指向全局window,改成普通函数后this指向cat对象。 const cat = { lives: 9, jumps: () => { this.lives--; } }; //例2,此时this也是指向window,不能动态监听button,改成普通函数后this指向按钮对象。 var button = document.getElementById("press"); button.addEventListener("click", () => { this.classList.toggle("on"); }); ~~~ ## 说说你对作用域链的理解 *** 参考答案:作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到 window 对象即被终止,作用域链向下访问变量是不被允许的。 ## 在 js 中哪些会被隐式转换为 false***** 参考答案:Undefined、null、关键字 false、NaN、零、空字符串 ## 列举浏览器对象模型 BOM 里常用的至少 4 个对象,并列举 window 对象的常用方法至少 5 个? 参考答案: 对象:Window,document,location,screen,history,navigator。 方法:Alert(),confirm(),prompt(),open(),close()。 ## 对象浅拷贝和深拷贝有什么区别 ******* 参考答案:在`JS`中,除了基本数据类型,还存在对象、数组这种引用类型。 基本数据类型,拷贝是直接拷贝变量的值,而引用类型拷贝的其实是变量的地址。 ~~~ let o1 = {a: 1} let o2 = o1 ~~~ 在这种情况下,如果改变`o1`或`o2`其中一个值的话,另一个也会变,因为它们都指向同一个地址。 ~~~ o2.a = 3 console.log(o1.a) // 3 ~~~ 而浅拷贝和深拷贝就是在这个基础之上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有重新创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。 ## 如何编写高性能的 Javascript? ***** 这个在日常开发中去注意 参考答案: * 使用 DocumentFragment 优化多次 append * 通过模板元素 clone ,替代 createElement * 使用一次 innerHTML 赋值代替构建 dom 元素 * 使用 firstChild 和 nextSibling 代替 childNodes 遍历 dom 元素 * 使用 Array 做为 StringBuffer ,代替字符串拼接的操作 * 将循环控制量保存到局部变量 * 顺序无关的遍历时,用 while 替代 for * 将条件分支,按可能性顺序从高到低排列 * 在同一条件子的多( >2 )条件分支时,使用 switch 优于 if * 使用三目运算符替代条件分支 * 需要不断执行的时候,优先考虑使用 setInterval ## 使用 let、var 和 const 创建变量有什么区别 ******* 这个涉及变量提升, 还是挺重要的 参考答案: 用 var 声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,也可以是声明在任何函数外的变量。let 和 const 是块级作用域,意味着它们只能在最近的一组花括号(function、if-else 代码块或 for 循环中)中访问。 ~~~js function foo() { // 所有变量在函数中都可访问 var bar = "bar"; let baz = "baz"; const qux = "qux"; console.log(bar); // bar console.log(baz); // baz console.log(qux); // qux } console.log(bar); // ReferenceError: bar is not defined console.log(baz); // ReferenceError: baz is not defined console.log(qux); // ReferenceError: qux is not defined ~~~ ~~~js if (true) { var bar = "bar"; let baz = "baz"; const qux = "qux"; } // 用 var 声明的变量在函数作用域上都可访问 console.log(bar); // bar // let 和 const 定义的变量在它们被定义的语句块之外不可访问 console.log(baz); // ReferenceError: baz is not defined console.log(qux); // ReferenceError: qux is not defined ~~~ var 会使变量提升,这意味着变量可以在声明之前使用。let 和 const 不会使变量提升,提前使用会报错。 ~~~js console.log(foo); // undefined var foo = "foo"; console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initialization let baz = "baz"; console.log(bar); // ReferenceError: can't access lexical declaration 'bar' before initialization const bar = "bar"; ~~~ 用 var 重复声明不会报错,但 let 和 const 会。 ~~~js var foo = "foo"; var foo = "bar"; console.log(foo); // "bar" let baz = "baz"; let baz = "qux"; // Uncaught SyntaxError: Identifier 'baz' has already been declared ~~~ let 和 const 的区别在于:let 允许多次赋值,而 const 只允许一次。 ~~~js // 这样不会报错。 let foo = "foo"; foo = "bar"; // 这样会报错。 const baz = "baz"; baz = "qux"; ~~~ 解析:[参考](https://github.com/yangshun/front-end-interview-handbook/blob/master/Translations/Chinese/questions/javascript-questions.md#%E4%BD%BF%E7%94%A8letvar%E5%92%8Cconst%E5%88%9B%E5%BB%BA%E5%8F%98%E9%87%8F%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) ## prototype 和\_\_proto\_\_的关系是什么***** 参考答案: 所有的对象都拥有\_\_proto\_\_属性,它指向对象构造函数的 prototype 属性 ~~~ let obj = {} obj.__proto__ === Object.prototype // true function Test(){} var test = new Test() test.__proto__ == Test.prototype // true ~~~ 所有的函数都同时拥有\_\_proto\_\_和 protytpe 属性 函数的\_\_proto\_\_指向自己的函数实现 函数的 protytpe 是一个对象 所以函数的 prototype 也有\_\_proto\_\_属性 指向 Object.prototype ~~~ function func() {} func.prototype.__proto__ === Object.prototype // true ~~~ Object.prototype.\_\_proto\_\_指向 null ~~~ Object.prototype.__proto__ // null ~~~ ## 如何避免重绘或者重排?*** 参考答案: 1.分离读写操作 ~~~ var curLeft=div.offsetLeft; var curTop=div.offsetTop; div.style.left=curLeft+1+'px'; div.style.top=curTop+1+'px'; ~~~ 2.样式集中改变 ~~~ 可以添加一个类,样式都在类中改变 ~~~ 3.可以使用absolute脱离文档流。 4.使用 display:none ,不使用 visibility,也不要改变 它的 z-index 5.能用css3实现的就用css3实现。 ## forEach,map和filter的区别(哔哩哔哩)**** 参考答案: * filter函数,顾名思义,它是一个用来过滤的函数。他可以通过指定的过滤条件,删选出数组中符合条件的元素,并返回。 * map函数,这个函数与filter函数不同之处在于,filter()把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素。而map则会返回传入函数return的值。 * forEach函数,可以实现对数组的遍历,和map函数与filter函数不同的是它没有返回值。 ## delete 数组的 item,数组的 length 是否会 -1 ***** 参考答案:不会 解析: ### [](https://github.com/yisainan/web-interview/edit/master/content/js/js.md#delete-arrayindex)delete Array\[index\] ~~~js const arr = ['a', 'b', 'c', 'd', 'e']; let result = delete arr[1]; console.log(result); // true; console.log(arr); // ['a', undefined, 'c', 'd', 'e'] console.log(arr.length); // 5 console.log(arr[1]); // undefined ~~~ 使用delete删除元素,返回true和false, true表示删除成功,false表示删除失败。使用delete删除数组元素并不会改变原数组的长度,只是把被删除元素的值变为undefined。 ## 执行上下文**** 参考答案: 执行上下文可以简单理解为一个对象: 它包含三个部分: * 变量对象(VO) * 作用域链(词法作用域) * this指向 它的类型: * 全局执行上下文 * 函数执行上下文 * eval执行上下文 代码执行过程: * 创建 全局上下文 (global EC) * 全局执行上下文 (caller) 逐行 自上而下 执行。遇到函数时,函数执行上下文 (callee) 被push到执行栈顶层 * 函数执行上下文被激活,成为 active EC, 开始执行函数中的代码,caller 被挂起 * 函数执行完后,callee 被pop移除出执行栈,控制权交还全局上下文 (caller),继续执行 ## 怎样理解setTimeout 执行误差**** 参考答案:定时器是属于 宏任务(macrotask) 。如果当前 执行栈 所花费的时间大于 定时器 时间,那么定时器的回调在 宏任务(macrotask) 里,来不及去调用,所有这个时间会有误差。 解析:[参考](https://juejin.im/post/5cfc9d266fb9a07edb3939ea?utm_medium=hao.caibaojian.com&utm_source=hao.caibaojian.com) ## 为什么for循环嵌套顺序会影响性能?*** 参考答案:把循环次数大的放在内层,执行时间会比较短 ~~~js var t1 = new Date().getTime() for (let i = 0; i < 100; i++) { for (let j = 0; j < 1000; j++) { for (let k = 0; k < 10000; k++) {} } } var t2 = new Date().getTime() console.log('first time', t2 - t1) ~~~ | 变量 | 实例化(次数) | 初始化(次数) | 比较(次数) | 自增(次数) | | --- | --- | --- | --- | --- | | i | 1 | 1 | 10 | 10 | | j | 10 | 10 | 10 \* 100 | 10 \* 100 | | k | 10 \* 100 | 10 \* 100 | 10 \* 100 \* 1000 | 10 \* 100 \* 1000 | ~~~js for (let i = 0; i < 10000; i++) { for (let j = 0; j < 1000; j++) { for (let k = 0; k < 100; k++) { } } } var t3 = new Date().getTime() console.log('two time', t3 - t2) ~~~ | 变量 | 实例化(次数) | 初始化(次数) | 比较(次数) | 自增(次数) | | --- | --- | --- | --- | --- | | i | 1 | 1 | 1000 | 1000 | | j | 1000 | 1000 | 1000 \* 100 | 1000 \* 100 | | k | 1000 \* 100 | 1000 \* 100 | 1000 \* 100 \* 10 | 1000 \* 100 \* 10 | 解析:[參考](https://blog.csdn.net/weixin_42182143/article/details/98682537) ## typeof 与 instanceof 区别***** ~~~ 1、typeof返回结果是该类型的字符串形式表示【6】(number、string、undefined、boolean、function、object) 2、instanceof是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型。 ~~~ ## 微任务和宏任务**** 参考答案: ~~~js /* * 宏任务 * 分类: setTimeout setInterval requrestAnimationFrame * 1.宏任务所处的队列就是宏任务队列 * 2.第一个宏任务队列中只有一个任务: 执行主线程的js代码 * 3.宏任务队列可以有多个 * 4.当宏任务队列的中的任务全部执行完以后会查看是否有微任务队列如果有先执行微任务队列中的所有任务,如果没有就查看是否有宏任务队列 * * 微任务 * 分类: new Promise().then(回调) process.nextTick * 1.微任务所处的队列就是微任务队列 * 2.只有一个微任务队列 * 3.在上一个宏任务队列执行完毕后如果有微任务队列就会执行微任务队列中的所有任务 * */ console.log('----------------- start -----------------'); setTimeout(() => { console.log('setTimeout'); }, 0) new Promise((resolve, reject) => { for (var i = 0; i < 5; i++) { console.log(i); } resolve(); // 修改promise实例对象的状态为成功的状态 }).then(() => { console.log('promise实例成功回调执行'); }) console.log('----------------- end -----------------'); ~~~ ## JavaScript 中 undefined 和 not defined 的区别 **** 参考答案:undefined是没有初始化,not defined是没有声明 ## JavaScript怎么清空数组?***** 参考答案: 方法1 arrayList = \[\]; 直接改变arrayList所指向的对象,原对象并不改变。 方法2 arrayList.length = 0; 这种方法通过设置length=0 使原数组清除元素。 方法3 arrayList.splice(0, arrayList.length); ## 两种函数声明有什么区别?**** ~~~js var foo = function() { // Some code }; function bar() { // Some code }; ~~~ 参考答案: foo的定义是在运行时。想系统说明这个问题,我们要引入变量提升的这一概念。 我们可以运行下如下代码看看结果。 ~~~js console.log(foo) console.log(bar) var foo = function() { // Some code }; function bar() { // Some code }; ~~~ 输出为 ~~~ undefined function bar(){ // Some code }; ~~~ 为什么那?为什么 foo 打印出来是 undefined,而 bar打印出来却是函数? JavaScript在执行时,会将变量提升。 所以上面代码JavaScript 引擎在实际执行时按这个顺序执行。 ~~~js // foo bar的定义位置被提升 function bar() { // Some code }; var foo; console.log(foo) console.log(bar) foo = function() { // Some code }; ~~~ ## 什么是跨域?跨域请求资源的方法有哪些?*** 跨域一般我都是在后台搞, 前端部署到Nginx, 请求转发给后台, 或者 后台设置 cors. jsonp 基本不用了, 这个印象中只能get请求 参考答案: ~~~ (1)、porxy代理 定义和用法:proxy代理用于将请求发送给后台服务器,通过服务器来发送请求,然后将请求的结果传递给前端。 实现方法:通过nginx代理; 注意点:1、如果你代理的是https协议的请求,那么你的proxy首先需要信任该证书(尤其是自定义证书)或者忽略证书检查,否则你的请求无法成功。 (2)、CORS 【Cross-Origin Resource Sharing】 定义和用法:是现代浏览器支持跨域资源请求的一种最常用的方式。 使用方法:一般需要后端人员在处理请求数据的时候,添加允许跨域的相关操作。如下: res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8", "Access-Control-Allow-Origin":'http://localhost', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, Content-Type' }); (3)、jsonp 定义和用法:通过动态插入一个script标签。浏览器对script的资源引用没有同源限制,同时资源加载到页面后会立即执行(没有阻塞的情况下)。 特点:通过情况下,通过动态创建script来读取他域的动态资源,获取的数据一般为json格式。 实例如下: <script> function testjsonp(data) { console.log(data.name); // 获取返回的结果 } </script> <script> var _script = document.createElement('script'); _script.type = "text/javascript"; _script.src = "http://localhost:8888/jsonp?callback=testjsonp"; document.head.appendChild(_script); </script> 缺点:   1、这种方式无法发送post请求(这里)   2、另外要确定jsonp的请求是否失败并不容易,大多数框架的实现都是结合超时时间来判定。 ~~~ ## null 和 undefined 的区别?**** 参考答案: ~~~ 首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。 undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。 undefined 在 js 中不是一个保留字,这意味着我们可以使用 undefined 来作为一个变量名,这样的做法是非常危险的,它 会影响我们对 undefined 值的判断。但是我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。 当我们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。当我们使用双等 号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。 ~~~ 详细资料可以参考:[《JavaScript 深入理解之 undefined 与 null》](http://cavszhouyou.top/JavaScript%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E4%B9%8Bundefined%E4%B8%8Enull.html) ## JavaScript 原型,原型链? 有什么特点?*** 参考答案: ~~~ 在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对 象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当我们使用构造函数新建一个对象后,在这个对象的内部 将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说我们 是不应该能够获取到这个值的,但是现在浏览器中都实现了 __proto__ 属性来让我们访问这个属性,但是我们最好不要使用这 个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,我们可以通过这个方法来获取对 象的原型。 当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又 会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就 是我们新建的对象为什么能够使用 toString() 等方法的原因。 特点: JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与 之相关的对象也会继承这一改变。 ~~~ 详细资料可以参考:[《JavaScript 深入理解之原型与原型链》](http://cavszhouyou.top/JavaScript%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E4%B9%8B%E5%8E%9F%E5%9E%8B%E4%B8%8E%E5%8E%9F%E5%9E%8B%E9%93%BE.html) ## isNaN 和 Number.isNaN 函数的区别?**** 参考答案: ~~~ 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会 返回 true ,会影响 NaN 的判断。 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,这种方法对于 NaN 的判断更为 准确。 ~~~ ## 其他值到字符串的转换规则?**** 参考答案: ~~~ 规范的 9.8 节中定义了抽象操作 ToString ,它负责处理非字符串到字符串的强制类型转换。 (1)Null 和 Undefined 类型 ,null 转换为 "null",undefined 转换为 "undefined", (2)Boolean 类型,true 转换为 "true",false 转换为 "false"。 (3)Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。 (4)Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。 (5)对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()(Object.prototype.toString()) 来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会 调用该方法并使用其返回值。 ~~~ ## 其他值到数字值的转换规则?**** 参考答案: ~~~ 有时我们需要将非数字值当作数字来使用,比如数学运算。为此 ES5 规范在 9.3 节定义了抽象操作 ToNumber。 (1)Undefined 类型的值转换为 NaN。 (2)Null 类型的值转换为 0。 (3)Boolean 类型的值,true 转换为 1,false 转换为 0。 (4)String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。 (5)Symbol 类型的值不能转换为数字,会报错。 (6)对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。 为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有valueOf() 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。 如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。 ~~~ ## 其他值到布尔类型的值的转换规则?**** 参考答案: ~~~ ES5 规范 9.2 节中定义了抽象操作 ToBoolean,列举了布尔强制类型转换所有可能出现的结果。 以下这些是假值: • undefined • null • false • +0、-0 和 NaN • "" 假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。 ~~~ ## js 类型转换****** ![](https://img.kancloud.cn/bd/dc/bddcd6f4e0033721fcdf9b86a0cb0f7e_910x648.png) ## == vs === 有什么区别? ****** 对于`==`来说,如果对比双方的类型**不一样**的话,就会进行**类型转换**,这也就用到了我们上一章节讲的内容。 假如我们需要对比`x`和`y`是否相同,就会进行如下判断流程: 1. 首先会判断两者类型是否**相同**。相同的话就是比大小了 2. 类型不相同的话,那么就会进行类型转换 3. 会先判断是否在对比`null`和`undefined`,是的话就会返回`true` 4. 判断两者类型是否为`string`和`number`,是的话就会将字符串转换为`number` ~~~js 1 == '1' ↓ 1 == 1 ~~~ 5. 判断其中一方是否为`boolean`,是的话就会把`boolean`转为`number`再进行判断 ~~~js '1' == true ↓ '1' == 1 ↓ 1 == 1 ~~~ 6. 判断其中一方是否为`object`且另一方为`string`、`number`或者`symbol`,是的话就会把`object`转为原始类型再进行判断 ~~~js '1' == { name: 'yck' } ↓ '1' == '[object Object]' ~~~ ~~~! 思考题:看完了上面的步骤,对于 [] == ![] 你是否能正确写出答案呢? ~~~ 如果你觉得记忆步骤太麻烦的话,我还提供了流程图供大家使用: ![](https://img.kancloud.cn/d8/5f/d85f5933edfdac8e5c6672faa827aacf_1005x426.png) 当然了,这个流程图并没有将所有的情况都列举出来,我这里只将常用到的情况列举了,如果你想了解更多的内容可以参考[标准文档](https://262.ecma-international.org/5.1/#sec-11.9.1)。 对于`===`来说就简单多了,就是判断两者类型和值是否相同。 更多的对比可以阅读这篇[文章](https://link.juejin.cn/?target=https%3A%2F%2Ffelix-kling.de%2Fjs-loose-comparison%2F "https://felix-kling.de/js-loose-comparison/") ## || 和 && 操作符的返回值?***** 参考答案: || 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先进行 ToBoolean 强制类型转换,然后再执行条件 判断。 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。 && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。 || 和 && 返回它们其中一个操作数的值,而非条件判断的结果 ## 谈谈 This 对象的理解。***** 参考答案: this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模 式来判断。 1.第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。 2.第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。 3.第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。 4.第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。 这四种方式,使用构造器调用模式的优先级最高,然后是 apply 、 call 和 bind 调用模式,然后是方法调用模式,然后 是函数调用模式。 《JavaScript 深入理解之 this 详解》 ## javascript 代码中的 "use strict"; 是什么意思 ? 使用它区别是什么?**** 参考答案: 相关知识点: ~~~ use strict 是一种 ECMAscript5 添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行。 设立"严格模式"的目的,主要有以下几个: ~~~ * 消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为; * 消除代码运行的一些不安全之处,保证代码运行的安全; * 提高编译器效率,增加运行速度; * 为未来新版本的 Javascript 做好铺垫。 区别: * 1.禁止使用 with 语句。 * 2.禁止 this 关键字指向全局对象。 * 3.对象不能有重名的属性。 回答: ~~~ use strict 指的是严格运行模式,在这种模式对 js 的使用添加了一些限制。比如说禁止 this 指向全局对象,还有禁止使 用 with 语句等。设立严格模式的目的,主要是为了消除代码使用中的一些不安全的使用方式,也是为了消除 js 语法本身的一 些不合理的地方,以此来减少一些运行时的怪异的行为。同时使用严格运行模式也能够提高编译的效率,从而提高代码的运行速度。 我认为严格模式代表了 js 一种更合理、更安全、更严谨的发展方向。 ~~~ 详细资料可以参考:[《Javascript 严格模式详解》](http://www.ruanyifeng.com/blog/2013/01/javascript_strict_mode.html) ## instanceof 的作用?**** 参考答案: ~~~js // instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。 // 实现: function myInstanceof(left, right) { let proto = Object.getPrototypeOf(left), // 获取对象的原型 prototype = right.prototype; // 获取构造函数的 prototype 对象 // 判断构造函数的 prototype 对象是否在对象的原型链上 while (true) { if (!proto) return false; if (proto === prototype) return true; proto = Object.getPrototypeOf(proto); } } ~~~ 详细资料可以参考:[《instanceof》](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/instanceof) ## 谈一谈浏览器的缓存机制?**** 参考答案: ~~~ 浏览器的缓存机制指的是通过在一段时间内保留已接收到的 web 资源的一个副本,如果在资源的有效时间内,发起了对这个资源的再一次请求,那么浏览器会直接使用缓存的副本,而不是向服务器发起请求。使用 web 缓存可以有效地提高页面的打开速度,减少不必要的网络带宽的消耗。 web 资源的缓存策略一般由服务器来指定,可以分为两种,分别是强缓存策略和协商缓存策略。 使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必再向服务器发起请求。强缓存策略可以通过两种方式来设置,分别是 http 头信息中的 Expires 属性和 Cache-Control 属性。 服务器通过在响应头中添加 Expires 属性,来指定资源的过期时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发送请求。这个时间是一个绝对时间,它是服务器的时间,因此可能存在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用户可以对客户端时间进行修改的情况,这样就可能会影响缓存命中的结果。 Expires 是 http1.0 中的方式,因为它的一些缺点,在 http 1.1 中提出了一个新的头部属性就是 Cache-Control 属性, 它提供了对资源的缓存的更精确的控制。它有很多不同的值,常用的比如我们可以通过设置 max-age 来指定资源能够被缓存的时间 的大小,这是一个相对的时间,它会根据这个时间的大小和资源第一次请求时的时间来计算出资源过期的时间,因此相对于 Expires 来说,这种方式更加有效一些。常用的还有比如 private ,用来规定资源只能被客户端缓存,不能够代理服务器所缓存。还有如 n o-store ,用来指定资源不能够被缓存,no-cache 代表该资源能够被缓存,但是立即失效,每次都需要向服务器发起请求。 一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方式一起使用时,Cache-Control 的优先级要高于 Expires 。 使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。 如果资源发生了修改,则返回修改后的资源。协商缓存也可以通过两种方式来设置,分别是 http 头信息中的 Etag 和 Last-Modified 属性。 服务器通过在响应头中添加 Last-Modified 属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个 If-Modified-Since 的属性,属性值为上一次资源返回时的 Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种方法有一个缺点,就是 Last-Modified 标注的最后修改时间只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,那么文件已将改变了但是 Last-Modified 却没有改变, 这样会造成缓存命中的不准确。 因为 Last-Modified 的这种可能发生的不准确性,http 中提供了另外一种方式,那就是 Etag 属性。服务器在返回资源的时候,在头信息中添加了 Etag 属性,这个属性是资源生成的唯一标识符,当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,浏览器会在请求头中添加一个 If-None-Match 属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值来和资源当前的 Etag 的值来进行比较,以此来判断资源是否发生改变,是否需要返回资源。通过这种方式,比 Last-Modified 的方式更加精确。 当 Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上 Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag 属性。 强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。 ~~~ 详细资料可以参考:[《浅谈浏览器缓存》](https://segmentfault.com/a/1190000012573337)[《前端优化:浏览器缓存技术介绍》](https://juejin.im/post/5b9346dcf265da0aac6fbe57#heading-3)[《请求头中的 Cache-Control》](https://www.web-tinker.com/article/21221.html)[《Cache-Control 字段值详解》](https://juejin.im/post/5c2d6c9ae51d450cf4195a08) ## 简单谈一下 cookie ?**** 参考答案: ~~~ 我的理解是 cookie 是服务器提供的一种用于维护会话状态信息的数据,通过服务器发送到浏览器,浏览器保存在本地,当下一次有同源的请求时,将保存的 cookie 值添加到请求头部,发送给服务端。这可以用来实现记录用户登录状态等功能。cookie 一般可以存储 4k 大小的数据,并且只能够被同源的网页所共享访问。 服务器端可以使用 Set-Cookie 的响应头部来配置 cookie 信息。一条cookie 包括了9个属性值 name、value、expires、domain、path、secure、HttpOnly、SameSite、Priority。其中 name 和 value 分别是 cookie 的名字和值。expires 指定了 cookie 失效的时间,domain 是域名、path是路径,domain 和 path 一起限制了 cookie 能够被哪些 url 访问。secure 规定了 cookie 只能在确保安全的情况下传输,HttpOnly 规定了这个 cookie 只能被服务器访问,不能使用 js 脚本访问。SameSite 属性用来限制第三方 cookie,可以有效防止 CSRF 攻击,从而减少安全风险。Priority 是 chrome 的提案,定义了三种优先级,当 cookie 数量超出时低优先级的 cookie 会被优先清除。 在发生 xhr 的跨域请求的时候,即使是同源下的 cookie,也不会被自动添加到请求头部,除非显示地规定。 ~~~ 详细资料可以参考:[《HTTP cookies》](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies)[《聊一聊 cookie》](https://segmentfault.com/a/1190000004556040) ## 模块化开发怎么做?**** 参考答案: ~~~ 我对模块的理解是,一个模块是实现一个特定功能的一组方法。在最开始的时候,js 只实现一些简单的功能,所以并没有模块的概念 ,但随着程序越来越复杂,代码的模块化开发变得越来越重要。 由于函数具有独立作用域的特点,最原始的写法是使用函数来作为模块,几个函数作为一个模块,但是这种方式容易造成全局变量的污 染,并且模块间没有联系。 后面提出了对象写法,通过将函数作为一个对象的方法来实现,这样解决了直接使用函数作为模块的一些缺点,但是这种办法会暴露所 有的所有的模块成员,外部代码可以修改内部属性的值。 现在最常用的是立即执行函数的写法,通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染。 ~~~ 详细资料可以参考:[《浅谈模块化开发》](https://juejin.im/post/5ab378c46fb9a028ce7b824f)[《Javascript 模块化编程(一):模块的写法》](http://www.ruanyifeng.com/blog/2012/10/javascript_module.html)[《前端模块化:CommonJS,AMD,CMD,ES6》](https://juejin.im/post/5aaa37c8f265da23945f365c)[《Module 的语法》](http://es6.ruanyifeng.com/#docs/module) ## js 的几种模块规范?**** 参考答案: ~~~ js 中现在比较成熟的有四种模块加载方案。 第一种是 CommonJS 方案,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是 服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式 加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。 第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定 义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。 第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和 require.js 的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。参考60 第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。这种方案和上面三种方案都不同。参考 61。 ~~~ ## 如何判断当前脚本运行在浏览器还是 node 环境中?(阿里)** 参考答案: ~~~ typeof window === 'undefined' ? 'node' : 'browser'; 通过判断当前环境的 window 对象类型是否为 undefined,如果是undefined,则说明当前脚本运行在node环境,否则说明运行在window环境。 ~~~ ## js 的事件循环是什么?**** 参考答案: 相关知识点: ~~~ 事件队列是一个存储着待执行任务的队列,其中的任务严格按照时间先后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务。执行栈则是一个类似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,如果不为空的话,事件队列便将第一个任务压入执行栈中运行。 ~~~ 回答: ~~~ 因为 js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当异步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。 微任务包括了 promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。 宏任务包括了 script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲 染等。 ~~~ 详细资料可以参考:[《浏览器事件循环机制(event loop)》](https://juejin.im/post/5afbc62151882542af04112d)[《详解 JavaScript 中的 Event Loop(事件循环)机制》](https://zhuanlan.zhihu.com/p/33058983)[《什么是 Event Loop?》](http://www.ruanyifeng.com/blog/2013/10/event_loop.html)[《这一次,彻底弄懂 JavaScript 执行机制》](https://juejin.im/post/59e85eebf265da430d571f89)