前端面试题集-js基础-1

·  阅读 124

1、对项目工程化的理解

前端工程化的目的是为了提高开发效率,降低开发难度,个人认为主要应该从模块化组件化规范化自动化四个方面思考。模块化就是将一个大文件拆分成互相依赖的小文件,再进行统一的拼装和加载比如es6的import,export,commonjs,amd,cmd等,webpack的模块化加载。组件化就是对UI的拆分,规范化例如目录结构的定义,编码规范,(如命名规范,js规范,css代码书写风格)前后端接口规范,文档规范等,自动化比如自动化部署(压缩合并打包)等。

2、数据存储

有cookies、sessionStorage、localStorage 相同点:都存储在客户端 不同点:

  • 存储大小 cookie数据大小不能超过4k,sessionStorage和localStorage可以达到5M
  • 有效时间 localStorage 存储持久数据,浏览器关闭后数据也不会丢失,除非主动删除数据。 sessionStorage: 数据在当前浏览器窗口关闭后自动删除。 cookie:设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。
  • 数据与服务器之间的交互方式 cookie的数据会自动的传递到服务端,服务器也可以写cookie到客户端。 sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。

3、undefined与null的区别

  • Undefined:当声明了一个变量,未初始化时,得到的就是Undefined,
  • null:代表一个空对象指针,特殊的对象值 null和Undefined都表示“值得空缺”,你可以认为Undefined是表示系统级的,出乎意料的或者类似错误的值得空缺,而null是表示程序级的正常的,或在意料之中的值得空缺。

4、优雅降级和渐进增强

  • 优雅降级:一开始就构建完整功能,然后针对浏览器测试和修复
  • 渐进增强:一开始就针对低版本浏览器进行构建页面,完成基本功能,再针对高级浏览器进行优化,追加功能,以达到更好的体验

5、JS哪些操作会造成内存泄漏

内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存

  1. 意外的全局变量引起的内存泄漏

    • 例如 value=“嘻嘻嘻”,value成为一个全局变量,不会被回收
    • 解决:在js程序中开启严格模式‘use strict'可以有效避免上述问题
  2. 闭包引起的内存泄漏

  3. 没有清理的DOM元素引用

var refA = document.getElementById('refA'); 
document.body.removeChild(refA);
// #refA不能回收,因为存在变量refA对它的引用。将其对#refA引用释放,但还是无法回收#refA。
// 解决:refA=null
复制代码
  1. 被遗忘的定时器或回调

    • setInterval没有结束之前,回调函数里面的变量和回调函数本身都无法被回收
    • 解决:不需要的时候调用clearInterval
  2. 循环引用

解决:不使用的时候置为null

6、闭包

  • 定义:在局部作用域引用上层作用域(非全局)的变量。即当一个函数的返回值是另外一个函数,而返回的那个函数如果调用了其父函数内部的其他变量,如果返回的这个函数在外部被执行,就产生了闭包。
  • 表现形式:使函数外部能够调用函数内部定义的变量。
  • 优点:防止变量污染作用域
  • 缺点:滥用闭包,会造成内存泄漏,由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,不能滥用闭包,否则会造成网页的性能问题。解决方法是,在退出函数之前,将不使用的局部变量删除
var count=10function add(){ 
  var count=0;
  return function(){ 
    count+=1; 
    alert(count); 
  } 
} 
var s=add() 
s()//1 
s()//2
复制代码

7、slice substring substr的区别

  • slice()和substring()第二次参数指定的是字符串最后一个字符后面的位置;
  • substr()第二个参数指定返回的字符串个数;
  • 参数为正数时,slice和substring表现相同,但是参数为负数时,三者的处理则不同。
    • slice() 会将所有的负数于字符串的长度相加
    • substr() 会将第一个负参数与字符串长度相加,第二个负参数转化为 0
    • substring() 将所有的负参数转化为 0
// 参数为正数
var string = 'hello world'; 
console.log(string.slice(3)); 
//lo world 
console.log(string.substring(3)); 
//lo world 
console.log(string.substr(3)); 
//lo world 
console.log(string.slice(3, 7)); 
//lo w 
console.log(string.substring(3, 7)); 
//lo w 
console.log(string.substr(3, 7)); 
//lo worl

//参数为负数
var string = 'hello world'; 
// length = 11 
console.log(string.slice(-3)); 
//rld slice(8) 
console.log(string.substring(-3)); 
//hello world substring(0) 
console.log(string.substr(-3)); 
//rld substr(8)
console.log(string.slice(3, -4)); 
//lo w slice(3, 7) 
console.log(string.substring(3, -4)); 
//hel substring(3, 0)
console.log(string.substr(3, -4)); 
//'' substring(3, 0)
复制代码

8、对js中作用域的理解

  1. 全局作用域:就是在顶层环境中申明的变量都是全局作用域,他们的属性其实都在window对象下面。
  2. 函数作用域:在函数内部定义的变量都是函数作用域,只能在函数中访问到,当然也可以通过闭包来访问。除此之外,在其他地方是没法访问函数内部的变量的。
  3. 块级作用域:es6中新增的特性,弥补了以前只能使用立即执行函数来创建局部变量的缺陷。可以用let和const和{}创造块级作用域,声明的变量仅在块级作用域内有效

9、返回顶部的方法有哪些

  1. 利用 a 标签的锚点。在顶部放一个 a 标签 <a name="top">顶部</a>在需要回到顶部的位置放置一个 a 标签,地址为 top,<a href="#top">回到顶部</a>
  2. 利用 javascript 设置 scrollTop = 0,一般设置在最外层,即  document.body.scrollTop = document.documentElement.scrollTop = 0;就可以返回顶部。
  3. scrollTo(0,0) / scrollTo(x,y)方法滚动当前window中显示的文档,让文档中由坐标x和y指定的点位于显示区域的左上角.

10、你理解的"use strict";是什么?使用它有什么优缺点

  • 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
  • 消除代码运行的一些不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的Javascript做好铺垫
  • 此时全局中的this是undefined,而不是window。

11、对new操作符的理解,以及手写实现一个new。

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

new步骤:new Object()举例:

  1. 创建一个新对象
  2. 把新对象的__proto__指向构造函数的prototype
  3. 把构造函数里的作用域this赋给新对象,因此this就指向了这个新对象
  4. 并执行构造函数(为这个新对象添加属性),执行结果保存起来作为result
  5. 返回这个新对象。判断执行函数的结果是不是null或Undefined,如果是则返回之前的新对象,如果不是则返回result 模拟new:
function newNew(constructor, ...args){ 
    var newObj = {}
    //1. 创建一个新对象 
    newObj.__proto__ = constructor.prototype; 
    // 2. 把新对象的原型指向构造函数的prototype 
    var res = constructor.apply(newObj, arg) 
    // 3. 把构造函数里的this指向新对象 
    return res.instanceof Object ? res : newObj; 
    // 4. 返回新对象 
} 
var obj = newNew(constructorFunction, 'willian', 18) 
console.log(obj.name, obj.age); //'willian', 18
console.log(obj.say()) // Hello willian
复制代码

12、字面量和new出来的对象和 Object.create(null)创建出来的对象有什么区别

  • 字面量和new创建出来的对象会继承Object的方法和属性,他们的隐式原型会指向Object的显式原型。
  • Object.create(null)创建出来的对象原型为null,作为原型链的顶端,自然也没有继承Object的方法和属性。它会把你传入的第一个参数对象作为原型来继承,第二个参数是关于它自己的自有属性的描述。

13、.0.1 + 0.2、0.1 + 0.3和0.1 * 0.2分别等于多少?并解释下为什么

EcmaScrpt规范定义Number的类型遵循了IEEE754-2008中的64位浮点数规则定义的小数后的有效位数至多为52位导致计算出现精度丢失问题

而0.1转为二进制是一个无限循环数0.0001100110011001100......(1100循环)。由于只能存储52位尾数位,所以会出现精度缺失,把它存到内存中再取出来转换成十进制就不是原来的0.1了,

// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 + 0.0011001100110011001100110011001100110011001100110011010 = 0.0100110011001100110011001100110011001100110011001100111 
// 转成十进制正好是 0.30000000000000004 
0.1 + 0.2 === 0.30000000000000004 //true
0.1 + 0.2 === 0.3 //false
复制代码
  • 延伸:如何解决?
parseFloat((0.1+0.2).toFixed(10)) === 0.3//true
复制代码
  1. toFixed() 方法使用定点表示法来格式化一个数,会对结果进行四舍五入,返回以字符串表示
  2. parseFloat() 函数可解析一个字符串,并返回一个浮点数。

14、说说bind、call、apply的区别?并手写实现一个bind的方法

  • call和apply都是为了解决改变this的指向。作用都是相同的,只是传参的方式不同。
  • 除了第一个参数外,call可以接收一个参数列表,apply只接受一个参数数组。 bind绑定完之后返回一个新的函数,不执行。
//手动实现call 
Function.prototype.myCall = function (context) { 
    // 先判断调用myCall是不是一个函数 
    // 这里的this就是调用myCall的 
    if (typeof this !== 'function') {
        throw new TypeError("Not a Function") 
    } 
    // 不传参数默认为window
    context = context || window 
    // 保存this 
    context.fn = this 
    // 保存参数 
    let args = Array.from(arguments).slice(1) 
    // Array.from 把伪数组对象转为数组 
    // 调用函数 
    let result = context.fn(...args)
    delete context.fn return result 
} 
//手动实现apply 
Function.prototype.myApply = function (context) { 
    // 判断this是不是函数 
    if (typeof this !== "function") { 
        throw new TypeError("Not a Function") 
    } 
    let result 
    // 默认是window 
    context = context || window 
    // 保存this context.fn = this 
    // 是否传参 if (arguments[1]) { 
    result = context.fn(...arguments[1]) 
    } else { 
    result = context.fn() 
    } 
    delete context.fn return result 
} 
//手动实现bind 
Function.prototype.myBind = function(context){ 
    // 判断是否是一个函数 
    if(typeof this !== "function") { 
        throw new TypeError("Not a Function") 
    } 
    // 保存调用bind的函数
    const _this = this 
    // 保存参数 
    const args =Array.from(arguments).slice(1) 
    // 返回一个函数 
    return function F () { 
    // 判断是否被当做构造函数使用 
        if (this instanceof F) { 
            return _this.apply(this, args.concat([...arguments])) 
        } 
        return _this.apply(context,args.concat([...arguments])) 
        } 
    } 
}
复制代码

15、对arguments的理解,它是数组吗?

arguments是类数组,不是数组,但是它有数组类似的length属性 可以通过 Arrary.from(arguments) 转化为数组。也可以通过Array.prototype.slice.call(arguments),,你也可以简单的使用 [].slice.call(arguments)来代替。

16、对this的理解。

this表示当前对象,this的指向是根据调用的上下文来决定的,默认指向window对象。

  1. 在全局作用域下直接调用函数,this指向window
  2. 对象函数调用,哪个对象调用就指向哪个对象
  3. 使用new 实例化对象,在构造函数中的this指向实例对象、类里的this指向也是实例对象,如果是静态方法,this指向class本身
  4. 使用call或apply可以改变this的指向
  5. 箭头函数的this指向定义函数时所在作用域的this
//例子1
function func(){
    console.log(this) 
}
func() //window
//例子2
<input type="button"id="btnOK" value="OK"> 
<script>
var btnOK=document.getElementById("btnOK"); 
btnOK.οnclick=function(){ 
    console.log(this);
    //this指向的是btnOK对象 
} 
</script>
//例子3
var Show=function(){ 
   this.myName="Mr.Cao"; 
   //这里的this指向的是obj对象 
} 
var obj=new Show();
复制代码

17、为什么会有跨域,如何解决跨域问题?

浏览器为了安全,产生了同源策略,协议、域名、端口有一个不同,就会产生跨域。

解决方案:

  1. JSONP JSONP是通过动态创建<script>元素来使用的,为src指定一个跨域URL。URL携带一个callback,通过在页面顶层作用域书写callback函数,就能拿到响应的数据。JSONP响应加载到页面中以后,就会立即执行。它是利用了script标签不存在跨域的原理。
  • 优点:可以访问响应文本,支持浏览器和服务器的双向通信
  • 缺点:
    1. 只能发送GET请求。
    2. 如果其他域不安全,可能会在响应中夹带恶意代码。
    3. 要确认JSONP请求是否失败并不容易
function handleRes(res){ 
    alert(res) 
 }
var script=document.createElement('script')
script.src="http://freegeoip.net/json/?callback=handleRes"
document.body.insertBefore(script,document.body.firstChild)
复制代码
  1. CORS 浏览器发送请求时,需要额外附加一个Oigin头部,其中包含页面的源信息(协议。域名、端口)。以便服务器根据这个头部信息来决定是否给予响应。如果服务器认为这个请求可以接受,就在Access-Control-Allow-Origin头部中回发相同的源信息。如果是是公共资源,可以回发"*"。如果没有这个头部,或者头部资源不匹配,浏览器就会驳回请求。
Origin:http://www.nczonline.net
Access-Control-Allow-Origin:http://www.nczonline.net
复制代码
  • 优点:简单方便,支持post请求

  • 缺点:需要后端的配合,不支持老版浏览器

  • 延伸:

    1. Preflighted 请求(Firefox3.5+、safari4+、Chrome) 支持开发人员使用自定义的头部、get、post之外的方法,以及不同类型的主体内容。在使用下列高级选项发送请求时,就会像服务器发送一个preflight请求。这种请求使用OPTIONS方法。

    • Origin:与简单的请求相同,即请求页面的源信息
    • Access-Control-Request-Method:请求自身使用的方法。
    • Access-Control-Request-Headers:(可选)自定义的头部信息。

    发送这个请求时,服务器可以决定是否允许这种类型的请求,服务器通过在响应中发送如下头部与浏览器进行沟通

    • Access-Control-Allow-Origin:与简单的请求相同

    • Access-Control-Allow-Method:允许的方法

    • Access-Control-Allow-Headers:允许的头部

    • Access-Control-Max-Age:应该将这个Preflight请求缓存多长时间(s)

    1. 带凭据的请求:(Firefox3.5+、safari4+、Chrome)

    默认情况,跨域请求不提供凭据(cookie、Http认证以及客户端SSL证明等)。通过将withCredentials属性设置为true,可以指定某个请求应该发送凭据。如果服务器接受带凭据的请求,会用下面的HTTP头部来响应

    • Access-Control-Allow-Credentials:true
  1. 图像Ping 图像Ping是与服务器进行的简单、单向的跨域通信的一种方式.使用<img>标签。因为一个网页可以从任何网页中加载图像,不用担心跨域问题。可以动态创建图像,使用onload和onerror事件来确定是否接收到了响应。常用于追踪用户点击页面或动态广告曝光次数。缺点:一是只能发送Get请求,二是无法访问服务器的响应文本
var img=new Image() 
img.onload=img.onerror=function (){ 
    alert('done') 
} 
img.src='.xxxx'
复制代码
  1. webSockte Web Sockets在一个单独的持久连接上提供全双工,双向通信。在js中创建了Web Sockets之后,会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应之后,建立的连接会使用HTTP升级从HTTP协议交换为Web Socket协议。未加密的连接不再是http://,而是ws://;加密的连接也不是https://。而是wss://

使用自定义协议而非HTTP协议的好处是,能够在客户端和服务器之间发送非常少量的数据,而不用担心HTTP那样字节级的开销、由于传递的数据包很小,因此Web Sockets适合移动应用。

var socket=new WebSocket("ws://www.example.com/server.php") 
//实例化了Websocket对象后,浏览器就会马上尝试创建连接,有一个表示当前状态的属性-readyState 
//0-正在建立连接 
//1-已经建立连接 
//2-正在关闭连接 
//3-已经关闭连接 
socket.close() 
//关闭连接 
socket.send('hello') 
//像服务器发送数据,只能发送纯文本数据,复杂数据需要先序列化 socket.onmessage=function(event){ 
    //发送数据 
    var data=event.data
} 
socket.onopen=function(){
    //成功建立连接时触发 
    alert('connection established') 
} 
socket.onerror=function(){
    //发生错误时触发
    alert('connection error') 
} 
socket.onclose=function(){ 
    //连接关闭时触发
    alert('connection closed) 
}
复制代码
  1. nginx代理跨域
  • nginx模拟一个虚拟服务器,因为服务器与服务器之间是不存在跨域的,所以可以代理跨域。
  • 发送数据时 ,客户端->nginx->服务端
  • 返回数据时,服务端->nginx->客户端

18、说说你对IIFE的理解

IFE是立即执行函数,立即执行函数能够形成一个独立作用域,在这个函数通常会写一些依赖环境之类的东西,为了让一个匿名函数立即执行要满足2个条件:第一函数体后面一定要有小括号,第二不能是声明函数,一定是函数表达式,如果是函数声明,可以通过"()"、"+"、"-"、"void"、"new"等运算符将其转换为函数表达式,然后再加"()"立即执行。

什么时候使用?

  1. 当我们需要写一个 js 插件,并且复用率很高的时候,建议使用
  2. 如果命名的函数只需要使用一次,建议使用
  3. 独立模块,能做到各组合模块的低耦合,减少对全局作用域的污染

立即执行函数的写法:

(function (){})() 
(function (){}()) 
!funtion (){}() 
?funtion (){}()
复制代码

19、原生的字符串操作方法有哪些?请列举并描述其功能

  • String.concat() 连接两个字符串
  • String.substr(startIndex,length) 截取部分字符串,第一个参数是开始index,第二个是截取长度。
  • String.substring(startIndex,endIndex) 截取部分字符串,第一个是开始索引,第二个是结束索引。
  • String.toUpperCase() String.toLowerCase() 转为大小写
  • String.indexOf(searchString) 找对应的索引
  • string.split('')字符串转化成数组;
  • string.includes(val)包含某个字符串,返回布尔值;
  • string.startsWith(val)以...开始
  • string.endsWith(val)以...结束

20、请解释下NaN === NaN的结果

false,NaN不等于任何数包括他自己

21、几种创建对象的方式

  1. new Object()
  • 优点:足够简单。
  • 缺点每个对象都是独立的。
  1. 工厂模式
function createObj(name,age){ 
    var obj = {}; 
    obj.name=name;
    obj.age=age; 
    return obj 
}
var Anson = createObj('Anson', 18) 
console.log(Anson) //{name: "Anson", age: 18}
复制代码

优点是可以解决创建多个相似对象的问题,缺点是 无法识别对象的类型。

  1. 构造函数
function Person(name,age){ 
    this.name =name; 
    this.age=age; 
    this.sayName =function (){ 
    alert(this.name) 
    } 
} 
var person = new Person('小明',13); 
console.log(person); // Person {name: "小明", age: 13, sayName: ƒ}
复制代码

优点是可以创建特定类型的对象,缺点是 多个实例重复创建方法

4.(构造函数+原型)组合模式

function Person(name, age){ 
    this.name = name; 
    this.age = age; 
} 
Person.prototype.sayName = function (){ 
    alert(this.name) 
} 
var person = new Person('小白',18) 
console.log(person);
//Person {name: "小白", age: 18} __proto__ -> sayName: ƒ ()
复制代码

优点 多个实例引用一个原型上的方法 比较常用

  1. 动态原型
function Person(name,age){
this.name=name 
this.age =age 
    if(typeof this.sayName != 'function'){ 
        Person.prototype.sayName = function(){ 
            alert(this.name) 
        } 
    } 
}
var person = new Person('小红',15) 
console.log(person); //Person {name: "小红", age: 15} 动态创建sayName: ƒ ()
复制代码

优点 可以判断某个方法是否有效,来决定是否需要初始化原型,if只会在仅在碰到第一个实例调用方法时会执行,此后所有实例共享此方法

  1. 寄生构造函数模式 除了使用new操作符,其他的和工厂函数一样,
function Person(name,age,job){ 
    var o=new Object(); 
    o.name=name; 
    o.age=age; 
    o.job=job;
    o.sayName=function(){ 
        console.log(this.name) 
    } 
    return o; 
}
var friend=new Person("her",18,"Front-end Engineer"); 
friend.sayName(); //her
复制代码
  1. Object.create() 通过传入一个原型对象,创建一个新对象,使用现有的对象来提供新创建的对象的__proto__,实现继承。
const person = { 
    isHuman: false, 
    printIntroduction: function () { 
        console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`); 
    } 
}; 
const me = Object.create(person);
me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritt
me.printIntroduction(); // expected output: "My name is Matthew. Am I human? true"
复制代码

22、写一个使两个整数进行交换的方法(不能使用临时变量)

//1.利用运算符优先级和0*
b = a + 0 * (a = b);
//2.利用执行顺序
a = a + b; 
b = a - b;
a = a - b;
// es6解构
let a = 1, b= 2 
[a, b] = [b, a]
复制代码

23、说说你对事件冒泡机制的理解

事件传播的过程(事件流)分为捕获阶段、目标阶段和冒泡阶段。例如,当页面某一个元素被点击,首先是事件捕获阶段,window最先接收事件,然后一节一节往下捕获,最后由具体元素接收,然后再由具体元素一节一节网上,最后widow会再次接收事件,这就是冒泡阶段。事件捕获的意义在于,能够在目标获得点击事件之前截取事件,并对其处理。

并不是所有的事件都能冒泡,以下事件不能冒泡:blur,focus,load,unload

延伸:

  1. 阻止事件冒泡,首要要判断是否是IE
if(event && event.stopPropagation){ 
    // w3c标准 阻止冒泡机制 
    event.stopPropagation(); 
}else{ 
    // IE系列 IE 678 
    window.event.cancelBubble = true;
}
复制代码
  1. dom2级第三个参数
element.addEventListener(event,function(),false);//事件捕获
element.addEventListener(event,function(),true);//事件冒泡
复制代码
  1. 阻止默认行为
function stopDefault(e){ 
    if(e&&e.preventDefault){ 
        e.preventDefalut(); 
    }else{ 
        window.event.returnValue=false; 
    } 
}
复制代码

24、事件代理/事件委托

事件代理就是利用事件冒泡原理在父元素接收事件,js中提供了event对象,在这个对象中有target属性,通过target属性我们就能知道具体是哪个元素触发了事件。注意:非IE下的目标元素获取采用event.target获取,IE下采用window.event.srcElement获取

为什么使用事件代理?因为我们操作DOM是有代价的,应该尽量避免频繁的操作DOM元素,如果给DOM每一个元素都添加事件,会影响性能。

举个栗子:

<ul id='ul'>
    <li>111111</li> 
    <li>222222</li> 
    <li>333333</li> 
</ul> 
<button id='button'>添加元素</button> 

window.onload = function(){ 
    let oUl = document.getElementById('ul'); 
    let aLi = oUl.getElementsByTagName('li'); 
    let but = document.getElementById('button'); 
    let now = 3; 
    // 事件源:event对象,不管在哪个事件中,只要你操作的哪个元素就是事件源 
    // ie:window.event.srcElement 
    // 标准:event.target
    oUl.onmouseover = function(e){   
        let ev = e || window.event;     
        let target = ev.target || ev.srcElement;     
        if(target.nodeName.toLowerCase() == 'li'){         
            target.style.background = 'red';     
        } 
    } 
    oUl.onmouseout = function(e){ 
        let ev = e || window.event;   
        let target = ev.target || ev.srcElement;
        if(target.nodeName.toLowerCase() == 'li'){  
            target.style.background = '';  
    }
}
    but.onclick = function(){ 
        now ++;   
        let newLi = document.createElement('li');       
        newLi.innerHTML = 111111 * now;   
        oUl.appendChild(newLi);
    } 
}
复制代码

25、为什么说js是单线程,而不是多线程呢

JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?因此即便是 HTML5 提出的 Web Worker 也不允许操作 DOM。

26、document的load 和ready有什么区别

  • window.onload 在页面资源(比如图片和媒体资源,它们的加载速度远慢于DOM的加载速度)加载完成之后才执行
  • document.ready DOM加载完执行

27、举例子说明javascript的变量声明提升和函数声明提升

console.log(a);//undefined 变量a提升 var a = 10; fn();//666 函数能在声明之前调用说明函数声明提升了 function fn() { console.log('666') }
复制代码

28、如何让(a==1 && a==2 && a==3)的值为true,把"=="换成"==="后还能为true吗

let a = { 
value: 0, 
    valueOf() { 
        return ++this.value; 
    } 
} 
a == 1 && a == 2 && a == 3 // true
复制代码
  • === 严格相等,会比较两个值的类型和值
  • == 抽象相等,比较时,会先进行类型转换,然后再比较值
  • "==" 转化规则: 首先通过valueOf 转换,即 obj.valueOf()方法的返回值,如果 obj.valueOf()方法的返回值是原始类型,那么直接返回,如果不是,再通过 obj.toString()方法转换,如果obj.toString()返回的是原始类型,直接返回该值,如果还不是原始类型,抛出不能转换异常。

29、说说你对数据类型转换的理解是什么?类型转换的方法有哪些

显示转化:Boolean,Number,String, parseInt,parseFloat, toString, JSON.stringify

隐式转换:

  1. 运算符号, 流程控制(if,while)
  2. 字符串连接符(+),转换为String
  3. 关系运算符(>,<,>=,<=,==,===,!==)、算术运算符号(+,-,*,/,%,++,--),转换为Number
  4. 逻辑非运算符(!),转换为Boolean

隐式转换规则:

  • 数组、对象等复杂数据类型在隐式转换时会先使用valueOf()获取其原始值,如果原始值不是Number则调用toString()转成Sting,再转成Number
  • Boolean转换在以下几种情况下为false:0,-0,NaN,undefined,"",null,false
  • undefined与null的特殊情况
undefined == undefined // true 
undefined == null // true 
null == null // true
复制代码
  • NaN与任何数据比较都是false

30、说说你对base64的理解,它的使用场景有哪些

base64是一种常用的编码方法,是一种基于64个可打印字符来表示二进制数据的表示方法。

有些网络传送渠道并不支持所有的字节,例如传统的邮件只支持可见字符的传送,像ASCII码的控制字符就不能通过邮件传送。这样用途就受到了很大的限制,比如图片二进制流的每个字节不可能全部是可见字符,所以就传送不了。最好的方法就是在不改变传统协议的情况下,做一种扩展方案来支持二进制文件的传送。把不可打印的字符也能用可打印字符来表示,问题就解决了。

Base64编码主要用在传输、存储、表示二进制等领域,还可以用来加密,但是这种加密比较简单,只是一眼看上去不知道什么内容罢了,当然也可以对Base64的字符序列进行定制来进行加密。

应用场景:

  • 上传图片时 先将图片转化为base64 然后上传
  • 对于小图片转化为base64在页面展示,不用再用连接请求消耗资源
  • url 通过base64加密
  • 电子邮件的附件,因为一个附件数据往往是有不可见字符的
  • 证书,特别是根证书,因为它要在网上被很多人下载

31、Ajax请求中get和post方式有什么区别呢?分别在哪些场景下使用?

  1. GET请求会将参数跟在URL后进行传递,也就是会在url中显示,post请求时作为HTTP消息的实体内容发送给Web服务器的
  2. GET请求有数据长度限制,一般在2000个字符,而POST没有限制。
  3. GET方式请求的数据会被浏览器缓存起来,POST没有
  4. 在客户端使用get请求时,服务器端使用Request.QueryString来获取参数。客户端使用post请求时,服务器端使用Request.Form来获取参数
  5. get请求参数会在url中显示,容易被他人窃取,post在请求体中,不会被窃取,post比get更加安全
  6. get刷新、后退无害,post数据会被重新提交
  7. get方式只能支持ASCII字符,向服务器传中文字符可能会乱码,post支持标准字符集,可以正确传递中文字符
  8. get一般用于从服务器上获取资源,post一般用于修改服务器上的资源

32、.说说你对深浅拷贝的理解?并实现一个对数组和对象深拷贝的方法?

  • 浅拷贝: 浅拷贝只是拷贝基本类型的数据,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,因此存在父对象被篡改的可能,浅拷贝只复制指向对象的指针,而不复制对象本身,新旧对象还是共享一块内存。
// 简单的浅拷贝 
var a = 1; 
var b = a;
console.log(b) //1 
a = 2;//改变a的值 
console.log(b) //1
//对象的浅拷贝
var obj1={ value: 'a' } 
var obj2 = Object.assign({},obj1); 
// 展开运算符也可以实现对象的浅拷贝 let obj2={...obj1} 
obj2.value='b'; 
console.log(obj1); // { value: 'a' }
// 但是如果拷贝的源对象当中包含对象时,OBject.assign()方法只会拷贝对象的引用地址
var obj1={ value: 'a', obj3:{ value2: 'c' } } 
var obj2 = Object.assign({},obj1); 
obj2.obj3.value2='b'; 
console.log(obj1);//{ value: 'a', obj3:{ value2: 'b' } }
复制代码
  • 深拷贝: 如果要拷贝的对象中包含对象,就需要深拷贝了,创造一个一模一样的对象,新旧对象不共享内存。一般使用原生的方法
  1. JSON.parse(JSON.stringify(obj))。(缺点:只能处理可以被枚举的属性,且对象属性为undefined有问题)
  2. 递归拷贝 深拷贝就是能够真正意义上的数组和对象的拷贝,递归调用浅拷贝(深拷贝会另外创造一个一模一样的对象,新旧对象不共享内存,修改新对象不会改变元对象)
function deepClone(obj){ 
    let objClone = Array.isArray(obj)?[]:{}; 
    if(obj && typeof obj==="object"){ 
        for(key in obj){
            if(obj.hasOwnProperty(key)){ 
                //hasOwnProperty方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性 
                //判断ojb子元素是否为对象,如果是,递归复制 
                if(obj[key]&&typeof obj[key] ==="object"){ 
                objClone[key] = deepClone(obj[key]); 
            }else{ 
                //如果不是,简单复制
                objClone[key] = obj[key]; 
            }
        }
     } 
   }
    return objClone;
}
let a=[1,2,3,4], 
b=deepClone(a);
a[0]=2; 
console.log(a,b);
复制代码

33、js中=、==、===三个的区别是什么?并说明它们各自的工作过程

  • = 是赋值操作符。执行顺序是从右到左, 比如 a = b = c,先执行 b = c,再执行 a = b。结合其它算术运算符就是复合赋值运算符比如: +=,-=,*=。a += b 等价于 a = a + b,其它复合赋值运算符类似。
  • == 在 js 中叫不严格等于。也是用来比较左右操作数是否相等,但是它在比较时会自动进行类型转换,不严格。举个例子 ''0 == false" 返回的结果是 true,左右都被转换成数字进行比较 Number(0) 和 Number(false) 都返回 0,所以相等。
  • === 在 js 中叫严格等于。就是严格比较左右两个操作数的相等性,包括数据类型和数值,0 === false 结果是 false。
  • 建议:平时编码应该尽量使用 === 而不是 == 避免因为自动类型转换带来的逻辑 bug。

34、请快速答出此题的答案并解释:var x, y = 1; x + y = ?

x // => undefined 
y // => 1
x + y // => undefined + 1 => NaN
复制代码

35、举例说明数组和对象的迭代方法分别有哪些

  • 数组的迭代:

    • every():对数组中的每一项运行给定函数,如果每一项都返回true,则返回true,否则false;
    • some():对数组中的每一项运行给定函数,如果至少有一项返回true,则返回true,否则false;
    • filter():对数组中的每一项给定函数,返回值为true的项重新组成新的数组;
    • map():对数组中的每一项给定函数,返回每一项调用这个函数的结果;
    • forEach():对方法中的每一项运行给定函数。这个方法没有返回值
  • 对象的迭代:

    • 用for-in遍历对象:会将原型链上所有可枚举属性都遍历到
    • 用Object.keys()获取对象属性名的集合,只遍历自身的可枚举属性
    • Object.getOwnPropertyNames 遍历所有自身属性 包含不可枚举的
    • 使用Reflect.ownKeys(obj)遍历:返回一个数组,包含对象自身的所有属性,不管属性名是Symbol或字符串,也不管是否可枚举.

36、举例说明什么是decodeURI()和encodeURI()是什么

Global对象的encodeURI()和encodeURIComponent()方法可以对URI (通用资源标识符)进行编码,以便发送给浏览器。有效的URI中不能包含某些字符,例如空格。而这URI编码方法就可以对URI进行编码,它们用特殊的UTF-8编码替换所有无效的字符,从而让浏览器能够接受和理解。

  • encodeURI()和encodeURIComponent()

 encodeURI()主要用于整个URI(例如http://www.jxbh.cn/illegal value.htm),而encode-URIComponent()主要用于对URI中的某一段(例如前面URI中的illegal value.htm)进行编码。它们的主要区别在于,encodeURI()不会对本身属于URI的特殊字符进行编码,例如冒号、正斜杠、问号和井字号;而encodeURIComponent()则会对它发现的任何非标准字符进行编码。 使用encodeURI()编码后的结果是除了空格之外的其他字符都原封不动,只有空格被替换成了%20。而encodeURIComponent()方法则会使用对应的编码替换所有非字母数字字符。这也正是可以对整个URI使用encodeURI(),而只能对附加在现有URI后面的字符串使用encodeURIComponent()的原因所在。一般来说,我们使用encodeURIComponent()方法的时候要比使用encodeURI()更多,因为在实践中更常见的是对查询字符串参数而不是对基础URL进行编码.

  • decodeURI()和decodeURIComponent()

decodeURI()只能对使用encodeURI()替换的字符进行解码。例如, 它可将%20替换成一个空格,但不会对%23作任何处理,因为%23表示井字号(#),而井字号不是使用encodeUR工()替换的。同样地,decodeURIComponent()能够解码使用encodeURIComponent()编码的所有字符,即它可以解码任何特殊字符的编码。

var uri="http://www.jxbh.cn/illegal value.htm#start"; 
alert(encodeURI (uri)): 
//”http://www.jxbh.cn/illegal%20value.htm#start” 
alert(encodaURIComponent (uri)); 
//”http%3A%2F%2Fwww.jxbh.cn%2Fillegal%20value.htm%23start”

var uri=”http%3A%2F%2Fwww.jxbh.cn%2Fillegal%20value.htm%23start”; 
//http%3A%2F%2Fwww.jxbh.cn%2Fillegal value.htm%23 start  
alert( decodeURI(uri)); 
//http://www.jxbh.cn/illegal value.htm#start 
alert( decodeURIComponent (uri));
复制代码

37、举例说明js如何实现继承

1.原型链

  • 原理:重写子类的原型对象,让子类的原型对象指向父类的实例
  • 缺点:1、包含引用类型值得原型属性会被所有实例共享。2、在创建子类型的实例时,不能向父类型的构造函数中传递参数
function SuperType(){
    this.property=true 
} 
SuperType.prototype.getSuperValue=function(){
    return this.property 
} 
function SubType(){ 
    this.subproperty=false 
} 
SubType.prototype=new SuperType()
SubType.prototype.getSuperValue=function(){
    return this.subproperty
} 
var instance=new SubType() 
console.log(instance.getSuperValue()) //true
复制代码

2、借用构造函数

  • 原理:在子类型构造函数的内部调用父类型构造函数,apply/call
  • 优点:解决了实例共享引用类型值的问题,还可以在子类构造函数中向父类型构造函数中传参
  • 缺点:方法都在构造函数中定义,函数复用就无从谈起了
function SuperType(name){ 
    this.colors=['red','yellow','blue'] 
    this.name=name 
}
function SubType(){
    SuperType.call(this,'Nick')
} 
var instance1=new SubType()
instance1.colors.push('black') 
console.log(instance1.colors) //['red','yellow','blue','black'] 
console.log(instance1.name) //'Nick' var instance2=new SubType() 
console.log(instance2.colors) //['red','yellow','blue']
复制代码

3、组合继承

  • 原理:将原型链和借用构造函数的技术结合在一块,使用原型链实现对原型属性和方法的继承,通过构造函数实现对实例属性的继承
  • 缺点:调用两次超类型构造函数
function SuperType(name){
    this.colors=['red','yellow','blue'] 
    this.name=name 
}
SuperType.prototype.sayName=function(){ 
    return this.name
} 
function SubType(name,age){ 
    //继承属性 
    SuperType.call(this,name)
    this.age=age 
} 
//继承方法
SubType.prototype=new SuperType() 
SubType.prototype.constructor=SubType
SubType.prototype.sayAge=function(){
    return this.age
} 
var instance1=new SubType('Nick',29) 
instance1.colors.push('black') 
console.log(instance1.colors) //['red','yellow','blue','black'] 
console.log(instance1.name) //'Nick'
console.log(instance1.age) //'29'
复制代码

4、原型式继承

  • 原理:首先,必须有一个对象作为另一个对象的基础。使用Object.create()实现原型式继承。这个方法接收两个参数,一个用作新对象原型的对象和(可选)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()和object()方法的行为相同
  • 优点:方便简单。在没有必要兴师动众的创建构造函数,而只是想让一个对象与另一个对象保持类似的情况下,原型式继承完全可以胜任
  • 缺点:包含引用类型值的属性始终会共享相应的值。
var person={ 
    name:'nick', 
    friends:['lily','bolo','lining'],
} 
var anotherPerson=Object.create(person)
anotherPerson.name="grey" 
anotherPerson.push('rob')
console.log(person.friends) //['lily','bolo','lining','rob']
复制代码

5、寄生式继承

  • 原理:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象
  • 缺点:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数相似
function createAnother(original){ 
    var clone=object(original) 
    clone.sayHi=function(){ 
      alert('Hi')
    } 
    return clone 
}
复制代码

6、寄生组合式继承

  • 原理:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非是就是超类型原型的一个副本而已。本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型
function SuperType (name) { 
    this.name = name; 
    this.colors = ["red", "green", "blue"]; 
} 
SuperType.prototype.sayName = function () { 
    alert(this.name);
};
function SubType (name, age) { 
    SuperType.call(this, name); 
    this.age = age;
} 
// 下面这部分替代给子类原型赋值的过程,不调用父类构造函数,直接继承父类原型 
var prototype = Object.create(SuperType.prototype); 
prototype.constructor = SubType;
SubType.prototype = prototype; 
SubType.prototype.sayAge = function () { alert(this.age); }
复制代码

38、js延迟加载的方式有哪些

  • defer 属性:脚本会被延迟到DOM树构建好之后再执行
    • 不阻止解析 document, 并行下载 d.js, e.js
    • 即使下载完 d.js, e.js 仍继续解析 document
    • 按照页面中出现的顺序,在其他同步脚本执行后,DOMContentLoaded 事件前 依次执行 d.js, e.js。
<head> 
    <script src="d.js" defer="defer"></script>
    <script src="e.js" defer="defer"></script> 
</head> 
//虽然<script> 元素放在了<head>元素中,但包含的脚本将延迟浏览器遇到</html>标签后再执行。
复制代码
  • async 属性:不让页面等待脚本下载和执行,从而异步加载页面其他内容
    • 不阻止解析 document, 并行下载 b.js, c.js
    • 当脚本下载完后立即执行。(两者执行顺序不确定,执行阶段不确定,可能在 DOMContentLoaded 事件前或者后 )
<head>
    <script src="b.js" async></script>
    <script src="c.js" async></script>
</head> 
//不会阻塞其他资源下载,但是不能控制加载的顺序
复制代码
  • 动态创建DOM方式(使用最多):通过document.createElement("script")创建新的script标签,为新创建的元素的src属性赋值,值即为需要加载的js文件地址
//这些代码应被放置在</body>标签前(接近HTML文件底部)
<script type="text/javascript"function downloadJSAtOnload() {   
        var element = document.createElement("script");  
        element.src"defer.js"document.body.appendChild(element);  
    }   
    if (window.addEventListener) {
        window.addEventListener("load",downloadJSAtOnload, false);     
    }else if (window.attachEvent) {
        window.attachEvent("onload",downloadJSAtOnload);
    }else{     
        window.onload =downloadJSAtOnload;
    }
</script> 
复制代码
  • 使用jQuery的getScript方法
$.getScript("outer.js",function(){
     //回调函数,成功获取文件后执行的函数
     console.log("脚本加载完成") 
 });
复制代码
  • 使用setTimeout延迟方法,给网页加载留出时间
<script language="JavaScript" src="" id="my"></script>
<script>
    setTimeout("document.getElementById('my').src='t1.js'; ",3000); 
    //延时3秒
</script>
复制代码
  • 让JS最后加载

把js外部引入的文件放到页面底部,来让js最后引入,从而加快页面加载速度

39、解释下为什么{} + [] === 0为true

那个 {} 是空语句块而非空对象,不参与运算,但是如果写成 [ ] + { } 则结果为 "[object Object]"

在js中,加法运算的规则很简单,只会触发两种情况:

  1. number + number
  2. string + string
  • 所以,在JavaScript隐式转换规则中首先会推算两个操作数是不是number类型,如果是则直接运算得出结果。
  • 如果有一个数为string,则将另一个操作数隐式的转换为string,然后通过字符串拼接得出结果。
  • 如果为布尔值这种简单的数据类型,那么将会转换为number类型来进行运算得出结果。
  • 如果操作数为对象或者是数组这种复杂的数据类型,那么就将两个操作数都转换为字符串,进行拼接 。
[].toString=''
{}.toString='[object object]'
复制代码

40、准确说出'1,2,3,4'.split()的结果是什么(包括类型和值)

["1,2,3,4"] 结果为一个长度为1的数组 元素的值类型为字符串。

因为split 接收两个参数

  • 第一个参数是用于判断如何分割的,可接受字符串或者正则表达式,如果为空会将整个字符串不进行分割直接作为数组的单个元素返回。
  • 第二个参数是用于限制分割次数

image.png

41、你是如何更好地处理Async/Await的异常的

async function f() { 
  try { let response = await fetch('http://no-such-url');
  } catch(err) {
  alert(err); 
  }
} f();
复制代码

42、请说说json和jsonp的区别

  • json是一种轻量级的数据交换格式,易于阅读和编写,耶易于机器解析和生成,一般前后端之间传递数据是通过json类型来实现的。
  • jsonp是一种用于跨域的技术。通过插入script标签来引入js文件,客户端通过执行js文件中的函数,获取返回值来得到需要的数据。原理是script标签可以实现跨域,而且可以跨域执行js文件。

43、请详细描述AJAX的工作原理

AJAX是一种异步请求数据的web开发技术,在不需要重新刷新页面的情况下,ajax通过异步请求加载后台数据,并在网页上呈现出来。

基本原理是,通过XMLHttpRequest向服务器发送异步请求,获得服务器返回的数据,利用js更新页面。其核心功能在于XMLHttpRequest对象。IE使用ActiveXObject

var xhr=null;
if (window.XMLHttpRequest) {
    // 兼容 IE7+, Firefox, Chrome, Opera, Safari 
    xhr=new XMLHttpRequest(); 
} else{
    // 兼容 IE6, IE5
    xhr=new ActiveXObject("Microsoft.XMLHTTP");
} 
//设置请求参数 xhr.open(method,url,async); 
// method:请求的类型;GET 或 POST 
// url:文件在服务器上的位置 
// async:true(异步)或 false(同步) send(string);
//post请求时才使用字符串参数,否则不用带参数。 
//设置回调函数 
xhr.onreadystatechange=function(){
    //每当 readyState 改变时,onreadystatechange 函数就会被执行。 
    //0:未初始化 – 尚未调用.open()方法; 
    //1:启动 – 已经调用.open()方法,但尚未调用.send()方法;
    //2:发送 – 已经调用.send()方法,但尚未接收到响应;
    //3:接收 – 已经接收到部分响应数据;
    //4:完成 – 已经接收到全部响应数据,而且已经可以在客户端使用了; 
    if(xhr.readyState==4){ 
    // 从服务器的response获得数据 
        if(xhr.status==200){
            //判断响应报文的状态,若为200说明服务器正常运行并返回响应数据,     
            document.myForm.time.value=xhr.responseText;      
        } 
    } 
}
复制代码

44、函数声明与函数表达式有什么区别

//函数声明 
function fn() {} 
//函数表达式
const aa = function() {}
复制代码

区别:

  1. 函数声明,函数名必须有,在函数表达式中可以省略函数名
  2. 函数声明会被预解析,函数表达式不会
  3. 函数声明会函数提升,将函数声明提升到作用域最上层,而函数表达式则必须等到解释器执行到它所在的代码行才会被真正解释执行

45、document.write和innerHTML有什么区别

  • document.write() 将需要展示的内容添加到 HTML 文档流中。对于一个已经加载完成的页面,document.write() 会重新绘制整个页面。自然其性能就不是很好。
  • innerHTML 是替换某个元素中的内容,简单地认为是  标签中间的内容。也即只会影响到所指定的元素。

46、分析('b' + 'a' + +'a' + 'a').toLowerCase()返回的结果

那个多出来的 + 是一元操作符,操作数是后面那个 'a',它被转成了 number,但是由于'a'无法转为数字,因此为 NaN。因此加出来是 'baNaNa',toLowerCase 一下就变成 banana 了,'b'+'a' 为字符串拼接 => ba

47、保护js代码的方式有哪些?分别说说他们的原理是什么

  • 压缩:通过替换变量名等对代码进行压缩,但是不会改变代码结构。主要目的是压缩体积。
  • 混淆:降低代码的可读性,可以通过增加无用代码,删除注释缩进,对代码进行转义。
  • 加密:使用加密插件对代码进行可逆的加密操作,后续可通过使用密钥对密文进行解密。

48、JavaScript有几种类型值?能否画出它们的内存图

  • 基本数据类型:Undefined、Null、Boolean、String、Number、Symbol
  • 引用数据类型:Object

基本数据类型存放在栈中,可以直接操作保存在变量中的实际值

0

引用数据类型是存放在堆内存中的对象,变量其实是保存在栈内存中的一个指针,这个指针指向堆内存

0

49、说下你对柯里化函数(currying)的理解,它有什么运用场景

函数柯里化指的是将能够接收多个参数的函数转化为接收单一参数的函数,并且返回接收余下参数且返回结果的新函数的技术。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值,用于封装复用.

// 实现一个add方法,使计算结果能够满足如下预期: 
add(1)(2)(3) = 6; 
add(1, 2, 3)(4) = 10; 
add(1)(2)(3)(4)(5) = 15; 
function add() { 
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments); 
    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(...arguments); return _adder; 
    };
    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () { 
        return _args.reduce(function (a, b) {
            return a + b; 
        }); 
    }
    return _adder; 
} 
add(1)(2)(3) // 6
add(1, 2, 3)(4) // 10
add(1)(2)(3)(4)(5) // 15 
add(2, 6)(1) // 9

function createCurry(func, args) {   
    var argity = func.length;  
    var args = args || [];   
    return function () {    
        var _args = [].slice.apply(arguments);  
        args.push(..._args);       
        if (args.length < argity) { 
            return createCurry.call(this, func, args);   
        }        
        return func.apply(this, args); 
    } 
}
复制代码

50、请描述下函数的执行过程

  1. 创建函数,生成函数作用域
  2. 创建函数的执行上下文,压入执行上下文栈中
  3. 将函数的作用域复制给函数的执行上下文
  4. 生成活动对象,声明函数、变量、加入形参
  5. 将活动对象,放到作用域的最顶端
  6. 执行函数
  7. 执行栈 弹出函数执行上下文

51、分别写出防抖和节流的两个函数,并描述它们分别有什么运用场景

  • 函数节流: 频繁触发,但只在特定的时间内才执行一次代码
  • 函数防抖: 频繁触发,但只在特定的时间内没有触发执行条件才执行一次代码

函数节流:

  • 函数节流的应用场景一般是onrize,onscroll等这些频繁触发的函数,比如你想获取滚动条的位置,然后执行下一步动作,如果监听后执行的是Dom操作,这样的频繁触发执行,可能会影响到浏览器性能,甚至会将浏览器卡崩。所以我们可以规定他多少秒执行一次,这种方法叫函数节流
// 限制500ms执行一次
var type = false;
window.onscroll = function(){ 
    if(type === true) return; 
    type = true;
    setTimeout(()=>{
        console.log("要执行的事");
        type = false;
    },500) 
} 
// 限制500ms执行一次
var time = null;
window.onscroll = function(){
    let curTime = new Date(); 
    if(time==null){ 
        time = curTime;
    } 
    if((curTime-time)>500){
        console.log("要执行的事"); 
    } 
}

function throlle(fn){ 
    let flag=true //true表示可以执行
    return function(){ 
        if(!flag) return 
        flag=false
        setTimeout(function(){ 
            fn.apply(this,arguments)
            flag=true 
        },500)
    } 
}
复制代码

函数防抖:

  • 函数防抖的应用场景:输入框搜索自动补全事件,频繁操作点赞和取消点赞等等,需要获取最后一次操作结果并发送给服务器.
  • 实现原理:如果在500ms内频繁操作点赞或者取消点赞,则每次都会清除一次定时器然后重新创建一个。直到最后一次操作点赞或者取消点赞,然后等待500ms后发送ajax
var timer = null; 
function click(){ 
clearTimeout(timer);
timer = setTimeout(()=>{
    ajax(...); 
},500) }

function debounce(fn){ 
    let timer=null 
    return function(){
        clearTimeout(timer)
        timer=setTimeout(function(){ 
            fn.apply(this,arguments) 
        },500) 
    } 
}
复制代码

52、从输入URL到浏览器显示页面发生了什么

  1. 在浏览器中输入url(解析IP地址)
  2. 应用层DNS解析域名
  3. 应用层客户端发送HTTP请求
  4. 传输层TCP传输报文(3次握手)
  5. 网络层IP协议查询MAC地址
  6. 数据到达数据链路层
  7. 服务器接收数据
  8. 服务器响应请求
  9. 服务器返回相应文件
  10. 页面渲染:现代浏览器渲染页面的过程是这样的:解析HTML以构建DOM树 –> 构建渲染树 –> 布局渲染树 –> 绘制渲染树。

在浏览器还没接收到完整的HTML文件时,它就开始渲染页面了,在遇到外部链入的脚本标签或样式标签或图片时,会再次发送HTTP请求重复上述的步骤。在收到CSS文件后会对已经渲染的页面重新渲染,加入它们应有的样式,图片文件加载完立刻显示在相应位置。在这一过程中可能会触发页面的重绘或重排。

53、请说下你对__proto__和prototype,原型链的理解

function Person() {} 
//Person 是一个构造函数
var person = new Person(); 
// new 创建了一个实例对象
person person.name = 'Kevin';
console.log(person.name)
复制代码

原型

  • 每一个构造函数都有一个prototype属性,指向另一个对象即实例原型。这个对象的所有属性和方法,都会被构造函数的实例继承。
  • 每一个原型都有一个constructor属性指向关联的构造函数
  • 每一个JavaScript对象(除了null)都具有一个属性,proto,这个属性指向该对象的原型

image.png

function Person() {} 
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true 
console.log(Person.prototype.constructor == Person) // true
复制代码

原型链

每个对象都有一个指向它的原型prototype对象的内部链接,每个原型对象又有自己的原型。直到某个对象的原型为null为止,组成这条链的最后一环。

image.png

54、请描述下什么是原型模式?它主要运用在哪些场景

原型模式是一种设计模式,就是创建一个共享的原型实例,通过拷贝这些原型创建新的对象,也就是创建一个对象作为另一个对象的Prototype属性。

  • 优点:用于创建重复的对象,可以提升性能。
  • 场景:
    1. 类初始化需要消耗很多资源
    2. 有多个调用者调用且每个调用者会修改其值,相当于保存一份原有的对象拷贝相同对象进行修改,即保护性拷贝
    3. 通过new对象时需要很多繁琐的准备或访问权限

55、exports和module.exports有什么区别?

  • 导出方式不一样
exports.xxx='xxx'
module.export = {}
复制代码
  • exports是module.exports的引用,两个指向的是用一个地址,而require能看到的只有module.exports

56、如何比较两个对象

有思路即可,步骤:

  1. 判断两个类型是否对象
  2. 判断两个对象key的长度是否一致,getOwnPropertyNames()
  3. 判断属性value值的数据类型,根据不同数据类型做比较
    • 是对象,重复这个步骤
    • 是数组,转字符串比较
    • 是基本类型,直接判断

57、Promise和Async处理失败的时候有什么区别

  • Promise错误可以在构造体里面被捕获,而async/await返回的是promise,可以通过catch直接捕获错误。
  • await 后接的Promise.reject都必须被捕获,否则会中断执行

58、如何判断一个属性是在实例上还是在原型上?

hasOwnProperty()方法可以检测一个属性是不是位于实例上,in操作符只要通过对象能够访问到的属性都会返回true。两者结合就可以判断

59、常见兼容性问题?

(1)事件对象兼容

e = e || window.event;

(2)阻止事件冒泡兼容

event.stopPropagation? event.stopPropagation():event.cancelBubble=true;

(3)阻止默认行为兼容

evt.preventDefault?evt.preventDefault():evt.returnValue=false;

(4)事件监听兼容

addEventListener ? addEventListener : attachEvent()

removeEventListener() ? removeEventListener() : detachEvent()

(5)事件目标对象兼容

var src = event.target || event.srcElement

60、AJAX 的局限性?

  • AJAX 不支持浏览器 back 按钮。
  • 安全问题 AJAX 暴露了与服务器交互的细节。
  • 对搜索引擎的支持比较弱。不会执行你的 JS 脚本,只会操作你的网页源代码;
  • 跨域请求有一定限制。解决方式:jsonp; 延伸:如何解决ajax无法后退的问题?

html5引入了新的API,即history.pushState,history.replaceState,可以改变浏览器的历史记录。使用history如何实现浏览器的前进后退

  • history.back() //返回上一页
  • history.forward() //去到下一页
  • history.go(n)
  • history管理页面跳转 history.pushState,history.replaceState

61、如何判断当前脚本运行在浏览器还是node环境中

通过判断Global对象是否为window,如果不为window,当前脚本没有运行在浏览器中

62、为什么要用多个域名来存储资源

  • CDN缓存更方便,cdn使用户就近获取资源,静态内容和动态内容分服务器存放,使用不同的服务器处理请求。处理动态内容的只处理动态内容,不处理别的,提高效率,这样使得CDN(内容分发网络)缓存更方便
  • 节省cookie带宽,跨域不会传cookie
  • 节约主域名的连接数,优化页面响应速度
  • 防止不必要的安全问题

63、数组的常用方法

改变原数组的方法

  • splice() 添加/删除数组元素 语法:arrayObject.splice(index,howmany,item1,.....,itemX) 参数: 1.index:必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。 2.howmany:可选。要删除的项目数量。如果设置为 0,则不会删除项目。 3.item1, ..., itemX:可选。向数组添加的新项目。 返回值: 如果有元素被删除,返回包含被删除项目的新数组。

  • sort() 数组排序 语法:arrayObject.sort(sortby) 参数: 1.sortby 可选。规定排序顺序。必须是函数。。 返回值: 返回包排序后的新数组。

  • pop() 删除一个数组中的最后的一个元素 返回值: 返回被删除的元素。

  • shift() 删除数组的第一个元素 返回值: 返回被删除的元素。

  • push() 向数组的末尾添加元素

语法:arrayObject.push(newelement1,newelement2,....,newelementX) 参数: 1.newelement1 必需。要添加到数组的第一个元素。 2.newelement2 可选。要添加到数组的第二个元素。 3.newelementX 可选。可添加若干个元素。 返回值: 返回被删除的元素。

  • unshift() 向数组的开头添加一个或更多元素 语法:arrayObject.unshift(newelement1,newelement2,....,newelementX) 参数: 1.newelement1 必需。要添加到数组的第一个元素。 2.newelement2 可选。要添加到数组的第二个元素。 3.newelementX 可选。可添加若干个元素。 返回值: arrayObject 的新长度。。
  • reverse() 颠倒数组中元素的顺序

语法:arrayObject.reverse() 返回值: 颠倒后的新数组。

  • copyWithin() 指定位置的成员复制到其他位置

语法:array.copyWithin(target, start = 0, end = this.length) 参数: 1.target(必需):从该位置开始替换数据。如果为负值,表示倒数。 2.start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。 3.end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。 返回值: 返回当前数组。

  • fill() 填充数组

语法:array.fill(value, start, end) 参数: 1.value 必需。填充的值。 2.start 可选。开始填充位置。 3.end 可选。停止填充位置 (默认为 array.length) 返回值: 返回当前数组。


不改变原数组的方法

  • slice() 浅拷贝数组的元素

语法:array.slice(begin, end); 参数: 1.begin(可选): 索引数值,接受负值,从该索引处开始提取原数组中的元素,默认值为0。 2.end(可选):索引数值(不包括),接受负值,在该索引处前结束提取原数组元素,默认值为数组末尾(包括最后一个元素)。 返回值: 返回一个从开始到结束(不包括结束)选择的数组的一部分浅拷贝到一个新数组对象,且原数组不会被修改。

  • join() 数组转字符串

语法:array.join(str) 参数: 1.str(可选): 指定要使用的分隔符,默认使用逗号作为分隔符。 返回值: 返回生成的字符串。

  • concat() 合并两个或多个数组

语法:var newArr =oldArray.concat(arrayX,arrayX,......,arrayX) 参数: 1.arrayX(必须):该参数可以是具体的值,也可以是数组对象。可以是任意多个。 返回值: 返回返回合并后的新数组。

  • indexOf() 查找数组是否存在某个元素

语法:array.indexOf(searchElement,fromIndex) 参数: 1.searchElement(必须):被查找的元素 2.fromIndex(可选):开始查找的位置(不能大于等于数组的长度,返回-1),接受负值,默认值为0。 返回值: 返回下标

  • lastIndexOf() 查找指定元素在数组中的最后一个位置

语法:arr.lastIndexOf(searchElement,fromIndex) 参数: 1.searchElement(必须): 被查找的元素 2.fromIndex(可选): 逆向查找开始位置,默认值数组的长度-1,即查找整个数组。 返回值: 方法返回指定元素,在数组中的最后一个的索引,如果不存在则返回 -1。(从数组后面往前查找)

  • includes() 查找数组是否包含某个元素

语法:array.includes(searchElement,fromIndex=0) 参数: 1.searchElement(必须):被查找的元素 2.fromIndex(可选):默认值为0,参数表示搜索的起始位置,接受负值。正值超过数组长度,数组不会被搜索,返回false。负值绝对值超过长数组度,重置从0开始搜索。 返回值: 返回布尔

64、['1','2','3'].map(parseInt)结果

[1,NaN,NaN]

['1','2','3'].map(parseInt)相当于 ['1','2','3'].map((item,index)=>{ return parseInt(item,index) })

parseInt第二个参数,表示基数。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。3不是二进制数,所以也返回NaN

65、深度优先遍历和广度优先遍历

深度优先遍历:是从根节点开始,直到最后一个节点,然后回溯,继续追溯下一条路径,直到到达所有的节点,如此往复,直到没有路径为止。

let deepTravel=(node,nodeList=[]){ 
    if(node!==null){ 
        nodeList.push(node)
        let children=node.children
        for(let i=0;i<children.length,i++){
            deelTravel(children[i],nodeList)
        } 
    }
    return nodeList
}
复制代码

广度优先遍历:是从根节点开始,沿着图的宽度遍历节点,如果所有节点均被访问过,则算法终止

let wedthTravel=(node){ 
    let nodeList=[] 
    let stack=[]
    if(node){ 
        stack.push(node) 
        while(stack.length){
            let item=stack.shift()
            let children=item.children 
            nodeList.push(item) 
            for(let i=0;i<children.length;i++){
                stack.push(children[i])
            }
        } 
    }
    return nodeList
}
复制代码

66、ES5/ES6 的继承除了写法以外还有什么区别?

  1. 继承机制完全不同,es5是先创建子类实例对象的this,然后将父类方法赋到这个this上(Parent.apply(this))。es6是先在子类构造函数中用super,将父类实例对象的属性和方法,加到this上面,再在子类的构造函数中进行修改它。也因此,es5中array,error等原生构造函数无法继承而es6就可以自己定义这些原生构造函数。
  2. es6的类内部定义的所有方法都不可枚举,这在es5中默认是可枚举的,甚至可不可枚举都可以用defineProperty配置;
  3. es6内部默认使用严格模式;
  4. 类内不存在变量提升,这个跟继承有关,必须保证子类在父类之后定义,如果允许变量提升就乱套了;
  5. 类必须使用new调用,否则就会报错。而构造函数不用new也可以执行

补充:子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。ES5 是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。

67、异步笔试题

async function async1() { 
    console.log('async1 start');
    await async2(); 
    console.log('async1 end'); 
}
async function async2() { 
    console.log('async2');
}
console.log('script start');
setTimeout(function() { 
    console.log('setTimeout');
}, 0) 
async1(); 
new Promise(function(resolve) {
    console.log('promise1');
    resolve(); 
}).then(function() {
    console.log('promise2');
}); 
console.log('script end');
复制代码

start async1 start async2 promise1 script end async1 end promise2 setTimeout

68、解释并说明下列结果

let a = 0
let b = async () => { 
    a = a + await 10 
    console.log('2', a) 
}
b()
a++
console.log('1', a) 

// -> '1' 1 
// -> '2' 10
复制代码
  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
  • 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10

上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。

69、有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣

Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()

1.Object.prototype.toString.call()于所有基本的数据类型都能进行判断,即使是 null 和 undefined ,常用于判断浏览器内置对象

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]"
复制代码

2.instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。所有对象类型 instanceof Object 都是 true。

[] instanceof Array; 
// true 
[] instanceof Object; 
// true
复制代码

3.Array.isArray()用来判断对象是否为数组,Array.isArray()是ES5新增的方法,具有兼容性问题。当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。

70、观察者模式和发布-订阅模式的差异

image.png

  1. 在观察者模式中,观察者是知道目标对象的,由具体目标调度。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。意思就是发布者和订阅者不知道对方的存在。需要一个第三方组件,叫做信息中介,它将订阅者和发布者串联起来,它过滤和分配所有输入的消息
  2. 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。观察者和被观察者,是松耦合的关系。发布者和订阅者,则完全不存在耦合
  3. 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
  4. 观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。

71、js的各种位置,比如clientHeight,scrollHeight,offsetHeight ,以及scrollTop, offsetTop,clientTop的区别?

参考回答:

  • clientHeight:表示的是可视区域的高度,不包含border和滚动条
  • offsetHeight:表示可视区域的高度,包含了border和滚动条
  • scrollHeight:表示了所有区域的高度,包含了因为滚动被隐藏的部分。
  • clientTop:容器内部相对于容器本身的top偏移,实际就是边框border的厚度,在未指定的情况下一般为0
  • scrollTop:滚动后被隐藏的高度,获取对象相对于由offsetParent属性指定的父坐标(css定位的元素或body元素)距离顶端的高度。
  • offsetTop:当前元素顶部距离父元素顶部的距离,和有没有滚动条没有关系

72、进程和线程是什么

  1. 计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
  2. 假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。
  3. 进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
  4. 一个车间里,可以有很多工人。他们协同完成一个任务。
  5. 线程就好比车间里的工人。一个进程可以包括多个线程
  6. 车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
  7. 可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
  8. 一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
  9. 还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
  10. 这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做信号量,用来保证多个线程不会互相冲突。
  11. 操作系统的设计,因此可以归结为三点: (1)以多进程形式,允许多个任务同时运行; (2)以多线程形式,允许单个任务分成不同的部分运行; (3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

总结:

  • 进程是操作系统进行资源分配的最小单位,线程是程序运行的最小单位。
  • 一个进程可以有多个线程,他们可以共享内存
  • 进程之间相互独立,都有自己的ip地址,线程必须依赖进程而存在
  • 进程的开销会比较大一些
  • 一个进程崩溃之后,在系统保护模式下,不会对其他进程产生影响。一个线程崩溃之后,整个进程都会死掉

73、Number() 的存储空间是多大?如果后台发送了一个超过最大自己的数字怎么办

Number类型的最大值为2的53次方,即9007199254740992,如果超过这个值,比如900719925474099222,那么得到的值会不精确,也就是900719925474099200

74、NaN 是什么,用 typeof 会输出什么?

NaN 不是一个数字 typeof输出number

75、如何判断一个对象是不是空对象?

  1. 将json对象转化为json字符串,再判断该字符串是否为"{}"
var data = {}; 
var b = (JSON.stringify(data) == "{}"); 
alert(b);//true
复制代码
  1. for in 循环判断
var obj = {}; 
var b = function() { 
    for(var key in obj) {
        return false; 
    } 
    return true;
}
alert(b());//true
复制代码
  1. jquery的isEmptyObject方法

此方法是jquery将2方法(for in)进行封装,使用时需要依赖jquery

var data = {}; 
var b = $.isEmptyObject(data); 
alert(b);
//true
复制代码
  1. Object.getOwnPropertyNames()方法

此方法是使用Object对象的getOwnPropertyNames方法,获取到对象中的属性名,存到一个数组中,返回数组对象,我们可以通过判断数组的length来判断此对象是否为空

注意:此方法不兼容ie8,其余浏览器没有测试

var data = {}; 
var arr = Object.getOwnPropertyNames(data); 
alert(arr.length == 0);//true
复制代码
  1. 使用ES6的Object.keys()方法

与4方法类似,是ES6的新方法, 返回值也是对象中属性名组成的数组

var data = {}; 
var arr = Object.keys(data); 
alert(arr.length == 0);//true
复制代码

76、如果一个构造函数,bind了一个对象,用这个构造函数创建出的实例会继承这个对象的属性吗?为什么?

不会继承,因为根据 this 绑定四大规则,new 绑定的优先级高于 bind 显示绑定,通过 new 进行构造函数调用时,会创建一个新对象,这个新对象会代替 bind 的对象绑定,作为此函数的 this,并且在此函数没有返回对象的情况下,返回这个新建的对象.

77、token相对于cookie的区别

1.无状态

基于token的验证是无状态的,后端不需要记录token,每一个令牌都是独立的。服务器唯一的工作就是在成功的登陆请求上签署token,并验证传入的token是否有效。

2.安全 csrf

攻击者无法获取正确的token jwt加密

3.支持移动平台

在移动平台上,cookie是不被支持的。

4.性能:通过数据库查询session信息,要比做一次token验证和解析要费时

78、在JS中什么是面向对象程序设计,面向对象设计优点?

在JS中面向对象更多说的是通过构造函数或者class封装一个独立的功能,以达到代码的复用。

面向对象的三个特点:

  • 封装:通过对象把属性和方法封装起来,相似对象的话采用构造函数或者类new得到。
  • 继承:通过混合继承(构造函数和原型)的方式,可以达到属性和方法的复用。
  • 多态:通过对象属性覆盖,可以让继承的对象拥有更多行为。

79、前端性能优化

juejin.cn/post/696910…

80、对于前端安全,你了解多少?说说你对XSS和CSRF的理解

1. XSS攻击

跨站脚本攻击指的、是攻击者想尽一切办法将一些可执行的代码注入到网页中,一般有sql注入,js脚本注入。xss的危害相当大,如果页面可以随意执行别人不安全的js代码,轻则会使页面错乱,功能缺失,重则造成用户信息泄漏。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。为了不和层叠样式表 CSS 混淆,故将其缩写为 XSS。XSS 可以分为:存储型 XSS (也叫持久型 XSS)、反射型 XSS (也叫非持久型)。

  • 存储型

    存储型也就是攻击的代码被服务端写入进数据库中,这种攻击危害性很大,因为如果网站访问量很大的话,就会导致大量正常访问页面的用户都受到攻击。

    这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。具有攻击性的脚本被保存到了服务器并且可以被普通用户完整的从服务器取得并执行,从而获得了在网络上传播的能力。

  • 反射型

    反射型也叫非持久型,相比于前者危害就小一些,一般通过修改 URL 参数的方式加入攻击代码,诱导用户访问链接从而进行攻击。

    这种常见于通过 URL 传递参数的功能,如网站搜索、跳转等。由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。

如何防范?

  • 输入输出过滤

    一切用户输入皆不可信,在输出时进行验证,一般做法是将 ‘ ” < > & 这些个危险字符进行转义。 对于 URL 地址的转义可以使用 encodeURI,当你需要编码 URL 中的参数的时候,那么 encodeURIComponent 是最好方法。

  • Cookie 的 HttpOnly

    当用户的登录凭证存储于服务器的 session 中,而在浏览器中是以 cookie 的形式存储的。很多 XSS 攻击目标都是窃取用户 cookie 伪造身份认证。可以通过在 cookie 中设置 HttpOnly 属性,js 脚本将无法读取到 cookie 信息。

2. CSRF攻击

跨站请求伪造,冒充正确用户登录做一些操作。请求需要鉴权,比如携带token,或者利用seesion,cookie来鉴权。敏感信息提交可以使用验证码 image.png

如何防御?

  • 验证码:强制用户必须与应用进行交互,才能完成最终请求。此种方式能很好的遏制 CSRF,但是用户体验相对差。
  • 尽量使用 post ,限制 get 使用;上一个例子可见,get 太容易被拿来做 CSRF 攻击,但是 post 也并不是万无一失,攻击者只需要构造一个 form 就可以。
  • token:token 验证的 CSRF 防御机制是公认最合适的方案。
    • 用户使用用户名密码来请求服务器
    • 服务器进行验证用户的信息
    • 服务器通过验证发送给用户一个token
    • 客户端存储token,并在每次请求时附送上这个token值
    • 服务端验证token值,并返回数据,如果这个token不合法,那么服务器拒绝这个请求
fetch('api/user/1', { headers: { 'Authorization': 'Bearer ' + token } })
复制代码

延伸1: 为什么TOKEN就是安全的?

  • token 校验之所以能防御 csrf,是因为相信浏览器的同源策略。因为只有在同源的情况下,页面才能进行脚本操作和使用 js 获取 cookie 的操作,才能获取到 token。也就是说第三方网站是没有办法拿到 token 的。只有真正有权限的网站或页面才有办法取到 token,并将 token 传到服务端。所以服务端默认带有相应 token 的请求都是合法的请求。

延伸2:XSS与CSRF两者的区别?

  • CSRF:需要用户先登录网站A,获取cookie,利用A本身的漏洞,去请求网站A的api
  • XSS:不需要登录,是向网站A注入JS代码,然后执行,篡改网站A的内容

3. 中间人攻击

中间人攻击是攻击方同时与服务端和客户端建立起了连接,并让对方认为连接是安全的,但是实际上整个通信过程都被攻击者控制了。攻击者不仅能获得双方的通信信息,还能修改通信信息。

中间人攻击是攻击方同时与服务端和客户端建立起了连接,并让对方认为连接是安全的,但是实际上整个通信过程都被攻击者控制了。攻击者不仅能获得双方的通信信息,还能修改通信信息。

81、Promise 模拟终止

  1. 当新对象保持“pending”状态时,原Promise链将会中止执行。缺点是处于pending状态的promise里面的回调不会被释放

Promise.resolve().then(() => { console.log('ok1') return new Promise(()=>{}) // 返回“pending”状态的Promise对象 }).then(() => { // 后续的函数不会被调用 console.log('ok2') }).catch(err => { console.log('err->', err) })

2.promise.race, 也能达到后续的Promise不再执行

let p1 = new Promise((resolve, reject) => { resolve('ok1') }) let p2 = new Promise((resolve, reject) => { setTimeout(() => {resolve('ok2')}, 10) }) Promise.race([p2, p1]).then((result) => { console.log(result) //ok1 }).catch((error) => { console.log(error) })

82、普通 for 循环的性能与 forEach 的性能比较,请解释其中的原因。

1、for 循环没有任何额外的函数调用栈和上下文;forEach函数签名实际上是 array.forEach(function(currentValue, index, arr), thisValue)它不是普通的 for 循环的语法糖,对于每一项都有一个回调函数,所以就需要函数栈和执行上下文考虑进来,这里可能拖慢性能

2、由于forEach 里操作了toObject 以及判断是否终止循环条件,所以比for循环复杂,数据越多循环就越耗时!

  • 在10万这个级别下, forEach 的性能是 for的十倍
  • 在100万这个量级下, forEach 的性能是和for的一致
  • 在1000万级以上的量级上 , forEach 的性能远远低于for的性能

83、为什么基本类型数据能够调用toString方法?

在JS中为我们提供了三个包装类,通过这三个包装类可以将基本数据类型的数据转为对象 String() Number() Boolean()

var num = new Number(3); 
var str = new String("hello"); 
var bool = new Boolean(true);
复制代码

方法和属性只能添加给对对象,不能添加给基本数据类型。当我们对一些基本数据类型的值调用属性和方法时,浏览器会临时使用包装类将其转换为对象,然后再调用对象的属性和方法

84 、import和commonJS中require的区别:

  1. import是ES6标准中的模块化解决方案(因为浏览器支持情况不同,项目中本质是使用node中的babel将es6转码为es5再执行,import会被转码为require)。require是node中遵循CommonJS规范的模块化解决方案。
  2. ES6模块是编译时输出接口,CommonJS模块是运行时加载。
  3. ES6模块是动态引用,即导入和导出的值都指向同一个内存地址,所以导入的值会随着导出值变化。CommonJs的模块是对象。导出时是指拷贝,就算导出的值变化了,导入的值也不会变化,如果想要更新值就要重新导入。
  4. import语句导入同一个模块如果加载多次只执行一次,require语句导入次数和实际执行次数相同。
  5. import必须用在当前模块的顶层,如果在局部作用域内,会报错,es6这样的设计可以提高编译器效率,但没法实现运行时加载。require可以用在代码的任何地方。
  6. ES6模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。利用顶层的this等于undefined这个语法点,可以侦测当前代码是否在 ES6 模块之中。const isNotModuleScript = this !== undefined。require的模块中this指向window,this === window.

85、构造函数与普通函数的区别

构造函数也是一个普通的函数,创建方式和普通函数一样,但构造函数习惯上首字母大写 。构造函数和普通函数的区别在于:

  1. 作用不一样。构造函数用来新建实例对象
  2. 调用方式不一样,普通函数直接调用person(),构造函数需要使用new关键字调用new Person
  3. 构造函数在调用的时候,首先内部创建一个新对象,将新对象的_proto_指向构造函数的prototype(其实就是创建了一个实例),然后将构造函数的this指向新对象,最后返回新对象。普通函数,在调用的时候,内部不会创建新的对象,内部的this指向调用函数的对象,返回值由return决定
  4. 构造函数的返回值:有一个默认的返回值,新创建的对象(实例);当手动添加返回值后(return语句):
    • 返回值是基本数据类型-->真正的返回值还是那个新创建的对象(实例)
    • 返回值是复杂数据类型(对象)-->真正的返回值是这个对象

86、懒加载和预加载

图片加载的时机:

  1. 设置了display: none的img标签和元素背景图片,不会渲染但是会加载。
  2. 父元素div设置了display: none,那么这个父元素里面的子元素img标签会加载,父元素里面子元素背景图片不会加载
  3. 重复或者加载过的图片只会加载一次
  4. 不存在元素的背景图片不会加载
  5. 伪类,比如hover,在触发后才会加载

懒加载:

原理:img标签的src属性用来表示图片的URL,当这个属性值不为空时,浏览器就会根据这个值发送请求,如果没有src属性就不会发送请求。所以,在页面 加入时将img标签的src指向为空或者指向一个小图片(loading或者缺省图),将真实地址存在一个自定义属性data-src中,当页面滚动时,将可视区域的图片的src值赋为真实的值。

预加载:

简单理解,就是在使用该图片资源前,先加载到本地来,真正到使用时,直接从本地请求数据就行了。

  1. 单纯使用css:我们便可通过CSS的background属性将图片预加载到屏幕外的背景上。只要这些图片的路径保持不变,当它们在Web页面的其他地方被调用时,浏览器就会在渲染过程中使用预加载(缓存)的图片。使用该法加载的图片会同页面的其他内容一起加载,增加了页面的整体加载时间

  2. 使用JavaScript配合css背景图片。原理就是在DOM和CSS都加载完了,就是在ready方法里面给这些样式添加背景图片,这样就不会造成解析时间变长的问题。

  3. 用JavaScript创建图片。创建一个对象,就是new Image(),然后给这个对象赋值src,也可以使用数组去实现需要加载很多图片的时候。

  4. 使用Ajax。就是发起一个get请求,地址是这张图片,因为请求后浏览器会缓存,这张图片就预加载到了本地。

延伸:当img的src为空时还会请求嘛?

答:会

  • 在IE中,这样做会请求一次当前页面所在的目录。如在http://playgoogle.com/demo/a.html 中出现这种空src的标签,会导致重新请求一次://playgoogle.com/demo/
  • 在Safari 和 Chrome中,将请求当前页面本身。
  • 在Firefox 3.5以前的版本中,有和Safari同样的问题,但是在3.5中修正了这个BUG。
  • 在Opera 中,不会做额外的请求。

在一个访问量不高的网站中,多一个这样的请求也无所谓(甚至可以让你的网站浏览看上去翻番),但在一个千万级访问量甚至更高的WEB站点里,这样会导致你的服务器和带宽的成本显著增加。

87、websocket 为什么要加心跳? websocket 不是长连接吗,为什么还要加心跳去维持链接

在使用websocket的过程中,有时候会遇到网络断开的情况,但是在网络断开的时候服务器端并没有触发onclose的事件。这样会有:服务器会继续向客户端发送多余的链接,并且这些数据还会丢失。所以就需要一种机制来检测客户端和服务端是否处于正常的链接状态。因此就有了websocket的心跳了。还有心跳,说明还活着,没有心跳说明已经挂掉了。如果不存在检测,那么网络突然断开,造成的后果就是client、server可能还在傻乎乎的发送无用的消息,浪费了资源;

实现心跳检测的思路是:每隔一段固定的时间,向服务器端发送一个ping数据,如果在正常的情况下,服务器会返回一个pong给客户端,如果客户端通过onmessage事件能监听到的话,说明请求正常,这里我们使用了一个定时器,每隔3秒的情况下,如果是网络断开的情况下,在指定的时间内服务器端并没有返回心跳响应消息,因此服务器端断开了,因此这个时候我们使用ws.close关闭连接,在一段时间后(在不同的浏览器下,时间是不一样的,firefox响应更快),可以通过 onclose事件监听到。因此在onclose事件内,我们可以调用 reconnect事件进行重连操作。

var lockReconnect = false;//避免重复连接
var wsUrl = "wss://echo.websocket.org";
var ws; 
var tt;
function createWebSocket() {
    try {
        ws = new WebSocket(wsUrl); 
        init(); 
    } 
    catch(e) {
        console.log('catch');
        reconnect(wsUrl); 
    } 
} 
function init() {
    ws.onclose = function () { 
        console.log('链接关闭'); 
        reconnect(wsUrl); 
    };
    ws.onerror = function() {
        console.log('发生异常了');
        reconnect(wsUrl); 
    };
    ws.onopen = function () { 
        //心跳检测重置 
        heartCheck.start(); 
    };
    ws.onmessage = function (event) { 
        //拿到任何消息都说明当前连接是正常的 
        console.log('接收到消息');
        heartCheck.start(); 
    }
}
function reconnect(url) { 
    if(lockReconnect) {
        return; 
    }; 
    lockReconnect = true; //没连接上会一直重连,设置延迟避免请求过多 
    tt && clearTimeout(tt);
    tt = setTimeout(function () {
        createWebSocket(url); 
        lockReconnect = false;
    }, 4000);
}
var heartCheck = {
    timeout: 3000,
    timeoutObj: null, 
    serverTimeoutObj: null,
    start: function(){ 
        console.log('start'); 
        var self = this; 
        this.timeoutObj && clearTimeout(this.timeoutObj);
        this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj); 
        this.timeoutObj = setTimeout(function(){ 
            //这里发送一个心跳,后端收到后,返回一个心跳消息,
            console.log('55555'); 
            ws.send("123456789");
            self.serverTimeoutObj = setTimeout(function() {
                console.log(111); 
                console.log(ws); 
                ws.close(); 
                // createWebSocket(); 
            }, self.timeout);
        }, this.timeout)
     }
} 
createWebSocket(wsUrl);
复制代码

88、JSON.stringify的弊端

通俗点说,JSON.parse(JSON.stringfy(X)),其中X只能是Number, String, Boolean, Array, 扁平对象,即那些能够被 JSON 直接表示的数据结构。

  • 如果json里面有时间对象,则序列化结果:时间对象=>字符串的形式;
  • 如果json里有RegExp、Error对象,则序列化的结果将只得到空对象 RegExp、Error => {}
  • 如果json里有 function,undefined,则序列化的结果会把 function,undefined 丢失;
  • 如果json里有NaN、Infinity和-Infinity,则序列化的结果会变成null;
  • 如果json里有对象是由构造函数生成的,则序列化的结果会丢弃对象的 constructor;

89、什么是js的异步和同步?它们有什么区别?

因为JavaScript的单线程,因此同个时间只能处理同个任务,所有任务都需要排队,前一个任务执行完,才能继续执行下一个任务,但是,如果前一个任务的执行时间很长,比如文件的读取操作或ajax操作,后一个任务就不得不等着,拿ajax来说,当用户向后台获取大量的数据时,不得不等到所有数据都获取完毕才能进行下一步操作,用户只能在那里干等着,严重影响用户体验。

因此,JavaScript在设计的时候,就已经考虑到这个问题,主线程可以完全不用等待文件的读取完毕或ajax的加载成功,可以先挂起处于等待中的任务,先运行排在后面的任务,等到文件的读取或ajax有了结果后,再回过头执行挂起的任务,因此,任务就可以分为同步任务和异步任务

同步任务

    同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务

异步任务

    异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程,异步任务分为宏任务和微任务。

宏任务:

(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。宏任务包含:script(整体代码)setTimeoutsetIntervalI/OUI交互事件postMessageMessageChannelsetImmediate(Node.js 环境)

微任务:

microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。微任务包含:Promise.thenObject.observeMutationObserverprocess.nextTick(Node.js 环境)

事件循环:

单线程从从任务队列中读取任务是不断循环的,每次栈被清空后,都会在任务队列中读取新的任务,如果没有任务,就会等待,直到有新的任务,这就叫做任务循环,因为每个任务都是由一个事件触发的,因此也叫作事件循环。

运行机制:

  • 执行宏任务script,
  • 进入script后,所有的同步任务主线程执行
  • 所有宏任务放入宏任务执行队列
  • 所有微任务放入微任务执行队列
  • 先清空微任务队列,
  • 再取一个宏任务,执行
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

异步编程

callback、Promise、Generator、Async Await

90、meta viewport原理

移动端浏览器通常都在一个比屏幕更宽的虚拟窗口中渲染页面,这个虚拟窗口就是viewport,目的是正常展示没有做移动端适配的网页,可以让他们完整的展现给用户。我们有时用移动设备访问桌面版网页就会看到一个横向滚动条,这里可显示区域的宽度就是viewport的宽度。viewPort通常比屏幕宽,用户可以缩放和平移来查看页面的不同部分。

meta标签可以控制viewport的大小和缩放

<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
复制代码

91、首屏、白屏时间如何计算

白屏时间(First Paint):

是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。中间过程包括DNS查询、建立TCP链接、发送首个HTTP请求、返回HTML文档、HTML文档head解析完毕。因此影响白屏时间的因素:网络、服务端性能、前端页面结构设计。

通常认为浏览器开始渲染或者解析完的时间是白屏结束的时间点。所以我们可以在html文档的head中所有的静态资源以及内嵌脚本/样式之前记录一个时间点,在head最底部记录另一个时间点,两者的差值作为白屏时间

  • 白屏时间 = 页面开始展示的时间点 - 开始请求的时间点
<!DOCTYPE html> 
<html lang="en"> 
    <head>
        <meta charset="UTF-8"> 
        <meta name="viewport" content="width=device-width, initial-scale=1.0"> 
        <title>白屏时间计算-常规方法</title>
        <script>
            window.pageStartTime = Date.now() 
        </script>
        <link rel="stylesheet" href="https://b-gold-cdn.xitu.io/ionicons/2.0.1/css/ionicons.min.css">
        <link rel="stylesheet" href="https://b-gold-cdn.xitu.io/asset/fw-icon/1.0.9/iconfont.css">
        <script>
            window.firstPaint = Date.now() 
            console.log(`白屏时间:${window.firstPaint - window.pageStartTime}`) 
        </script> 
    </head>
    <body> 
        <div>这是常规计算白屏时间的示例页面</div>
    </body> 
</html>
复制代码

window.performance是一个浏览器中用于记录页面加载和解析过程中关键时间点的对象,放置在global环境下,计算白屏:responseStart - window.performance.timing.navigationStart

首屏时间(First Contentful Paint):

是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。

  • 首屏时间 = 首屏内容渲染结束时间点 - 开始请求的时间点

具体实现:

1、首屏模块标签标记法:

在业务中,较少使用这种算法,大多数页面需要使用接口,所以这种方法就太不常用。但是如果你的页面是静态页面,或者异步数据不影响整体的首屏体验,那么就可以使用这种办法

2、统计首屏内加载最慢的图片的时间:

拿到首屏内所有的图片,遍历它们,逐个监听图片标签的onload事件,并收集到它们的加载时间,最后比较得到加载时间的最大值。适用于首屏元素固定的页面,比如移动端更适用一点。

3、自定义首屏内容计算法:

这个方法主要考虑异步数据。就是根据首屏内接口计算比较得出最迟的时间

<!DOCTYPE html> 
<html lang="en">
<head> 
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>首屏时间计算-自定义模块计算法</title> 
    <script> 
        window.pageStartTime = Date.now() 
    </script>
</head> 
<body>
    <div class="module-1"></div> 
    <div class="module-2"></div> 
    <script type="text/javascript"> 
        setTimeout(() => { 
            // 假设这里异步加载首屏要显示的文章列表数据
            window.firstScreen = Date.now(); 
            console.log(window.firstScreen - window.pageStartTime) 
        }, 500) 
    </script> 
    <div class="module-3"></div>
</body> 
</html>
复制代码

performance.timing记录的是用于分析页面整体性能指标。如果要获取个别资源(例如JS、图片)的性能指标,就需要使用Resource Timing API。首屏时间:performance.getEntriesByName(“first-contentful-paint”)[0].startTime - navigationStart

使用场景:

对于交互性比较少的简单网页,由于加载比较快,所以二者区别不大,因此,可以根据喜好任选一种计算方式。对于大型的复杂页面,你会发现由于需要处理更多复杂的元素,白屏时间和首屏时间相隔比较远,这时候,计算首屏时间会更有用。

白屏和首屏优化

  • 白屏优化方案有:SSR、预渲染
  • 首屏优化方案有:CDN分发、静态资源缓存、后端在业务层的缓存、前端按需加载、减少http请求、http压缩

92、form表单可以跨域么

没有

浏览器同源策略本质是,一个域名下面的JS,没有经过允许是不能读取另外一个域名的内容,但是浏览器不阻止你向另外一个域名发送请求。所以form表单提交没有跨域问题,提交form表单到另外一个域名,form提交后原本的页面是无法得到响应的内容的,而ajax是想要读取响应内容,浏览器为了安全不允许这样的。浏览器不允许js访问别的域,但是浏览器却没有限制自己,img标签和script标签都是可以加载其他域的图片或者js文件。这就是jsonp的跨域嘛,利用浏览器的历史兼容性。

93、options请求是什么?

OPTIONS请求方法的主要用途有两个:

  1. 获取服务器支持的HTTP请求方法;
  2. 用来检查服务器的性能。例如:AJAX进行跨域请求时的预检,需要向另外一个域名的资源发送一个HTTP OPTIONS请求头,用以判断实际发送的请求是否安全。这得从浏览器同源策略和跨域说起。解决跨域问题的方法有很多种,CORS是比较好的解决方案,这个模式会有”预检"的请求,也就是正常请求之前的options请求

94、抽象类

用abstract关键字定义的抽象方法和抽象类。typescript中的抽象类是提供其它类的基类,不能直接被实例化;

abstract class Animal {  
    abstract eat():void; 
} 
// var a = new Animal(); 
// 错误的写法抽象类不能被实例化。
class Dog extends Animal { 
    constructor() {  
        super()  
    }  
    eat () {  
        console.log('抽象类的子类必须实现抽象类的抽象方法') 
    } 
}
复制代码

95、instanceof怎么实现

function myInstanceof(obj, constructor) {   
    // obj的隐式原型  
    let implicitPrototype = obj?.__proto__;    
    // 构造函数的原型   
    const displayPrototype = constructor.prototype;    
    // 遍历原型链 
    while (implicitPrototype) {    
        // 找到,返回true   
        if (implicitPrototype === displayPrototype) return true; 
        implicitPrototype = implicitPrototype.__proto__;   
    }    
    // 遍历结束还没找到,返回false  
    return false; 
}
复制代码

96、try-catch可以捕获setTimeout的异常嘛?promise的异常可以捕获嘛

try-catch 主要用于捕获异常,注意,这里的异常,是指同步函数的异常,如果 try 里面的异步方法出现了异常,此时catch 是无法捕获到异常的,原因是因为:当异步函数抛出异常时,对于宏任务而言,执行函数时已经将该函数推入栈,此时并不在 try-catch 所在的栈,所以 try-catch 并不能捕获到错误。对于微任务而言,比如 promise,promise 的构造函数的异常只能被自带的 reject 也就是.catch 函数捕获到。

解决方案:

  1. 对于同步函数,使用try-catch即可
  2. 对于异步函数-宏任务,window全局错误捕获函数onerror
try {
    setTimeout(function(){ 
        console.log(b);
    }, 0);
} catch (error) { 
    console.log(error); 
    // 这里是不会执行的 
} 
window.onerror = function() { 
    console.log(...arguments) 
}
复制代码
  1. 对于异步函数-微任务,js 有专门捕获没有写 catch 的 promise,如下:
window.addEventListener(
  'unhandledrejection',
  function() { console.log(...arguments) }
)
复制代码

更多知识点:

try-catch 中的异常只会抛出一层,即不会冒泡,也就是如果你有多层的 try-catch 然后异常已经被内层的 catch 捕获了,外层的 catch 是捕获不到异常的。解决方案是可以在内层的 catch 再手动 throw 出异常

try { 
    try { 
        throw new Error('oops');
    } catch (ex) {
        console.error('inner', ex.message); 
    } finally { 
        console.log('finally'); 
    } 
} catch (ex) { 
    onsole.error('outer', ex.message); 
} 
// Output:
// "inner" "oops" 
// "finally"
复制代码

97、for循环中使用setTimeout的问题

setTimeout延时器的运行机制:setTimeout会先将回调函数放到等待队列中,等待区域内其他主程序执行完毕后,按时间顺序先进先出执行回调函数。本质上是作用域的问题。

1、第一种情况

for (var i=1; i<=5; i++) { 
    setTimeout( function timer() {
        console.log( i ); 
    }, i*1000 );
}
复制代码

这是因为setTimeout是异步执行,每一次for循环的时候,setTimeout都执行一次,但是里面的函数没有被执行,而是被放到了任务队列里,等待执行。只有主线上的任务执行完,才会执行任务队列里的任务。也就是说它会等到for循环全部运行完毕后,才会执行fun函数,但是当for循环结束后此时i的值已经变成了6,因此虽然定时器跑了5秒,控制台上的内容依然是6。

注意:(for循环从开始到结束的过程,需要维持几微秒或几毫秒,当定时器跑完一秒之后for循环早已经做完了)

2、第二种情况

for (var i=1; i<=5; i++) { 
    (function() { 
        setTimeout( function timer() { console.log( i ); }, i*1000 ); 
    })(); 
}
复制代码

由setTimeout的运行机制可以知道,首先会运行外部的所有主程序,虽然for循环内形成了闭包,但是fun并没有发现一个实参所以跟第一个例子并无实际差别,仍然是连续输出5个6。

3、解决方案一

for (var i=1; i<=5; i++) {
    (function(j) { 
        setTimeout( function timer() { console.log( j ); }, j*1000 ); 
    })(i); 
}
复制代码

我们可以发现跟预期结果一致,依次输出1到5,因是因为实际参数跟定时器内部的i有强依赖。通过闭包,将i的变量驻留在内存中,当输出j时,引用的是外部函数的变量值i,i的值是根据循环来的,执行setTimeout时已经确定了里面的的输出了。

4、解决方案二

function timer(i) {
    setTimeout( console.log( i ), i*1000 ); 
} 
for (var i=1; i<=5;i++) { timer(i); }
复制代码

5、解决方案三

for (let i=1; i<=5; i++) { 
    setTimeout( function timer() { console.log( i ); }, i*1000 );
}
复制代码

因为for循环头部的let不仅将i绑定到for循环中,事实上它将其重新绑定到循环体的每一次迭代中,确保上一次迭代结束的值重新被赋值。setTimeout里面的function()属于一个新的域,通过var定义的变量是无法传入到这个函数执行域中的,通过使用let来声明块变量能作用于这个块,所以function就能使用i这个变量了;这个匿名函数的参数作用域和for参数的作用域不一样,是利用了这一点来完成的。这个匿名函数的作用域有点类似类的属性,是可以被内层方法使用的。

98、setTimeout(0)和setTimeout(2)之间的区别

定时器表面意思是过指定时间后执行,但是真正的含义是每过指定时间后,会有fn进入事件队列

  • setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行
  • setTimeout(fn,1) 1毫秒后执行,但是因为要先执行前面的宏任务,所以不一定就是在一毫秒后执行,取决于队列里任务执行的速度

99、Object,create做了什么

Object._create = function(obj){
    function F(){}; 
    // 创建了一个新的构造函数F F.prototype = obj; 
    // 然后将构造函数F的原型指向了参数对象obj 
    return new F(); 
    // 返回构造函数F的实例对象,从而实现了该实例继承obj的属性。
}
复制代码

100、为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?

  1. 可以发送get请求,且不需要获取和处理数据
  2. 可以跨域
  3. 执行过程无阻塞
  4. 相比XMLHttpRequest,性能更好
  5. gif体积小

篇幅原因 - 未完接下一篇

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改