初中级面试题(JS)

184 阅读34分钟

js是什么语言

JavaScript是一种直译式脚本语言,是一种动态类型、弱类型、基于原型的语言,内置支持类型。它的解释器被称为JavaScript引擎,是为浏览器的一部分,广泛用于客户端的脚本语言,最早是在HTML(标准通用标记语言下的一个应用)网页上使用,用来给HTML网页增加动态功能。 js语言是弱语言类型, 因此我们在项目开发中当我们随意更该某个变量的数据类型后 ,有可能会导致其他引用这个变量的方法中报错等等。

js数据类型

  • 简单数据类型:string,number、boolean、null、undefined、symbol、bigInt
  • 引用数据类型:object、function、array

JavaScript的基本类型和复杂类型是储存在哪里的

基本类型储存在栈中,但是一旦被闭包引用则成为常驻内存,会储存在内存堆中。复杂类型会储存在内存堆中。

判断数据类型的方法

/* typeof */
console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof undefined);       // undefined
console.log(typeof []);              // object 
console.log(typeof {});              // object
console.log(typeof function(){});    // function
console.log(typeof null);            // object
  • 优点:能够快速区分基本数据类型 。
  • 缺点:不能将Object、Array和Null区分,都返回object。
/* instanceof */
console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false  
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true
  • 优点:能够区分Array、Object和Function,适合用于判断自定义的类实例对象.
  • 缺点:Number,Boolean,String基本数据类型不能判断
/** Object.prototype.toString.call() */
var toString = Object.prototype.toString;
 
console.log(toString.call(2));                      //[object Number]
console.log(toString.call(true));                   //[object Boolean]
console.log(toString.call('str'));                  //[object String]
console.log(toString.call([]));                     //[object Array]
console.log(toString.call(function(){}));           //[object Function]
console.log(toString.call({}));                     //[object Object]
console.log(toString.call(undefined));              //[object Undefined]
console.log(toString.call(null));                   //[object Null]
  • 优点:精准判断数据类型。
  • 缺点:写法繁琐不容易记,推荐进行封装后使用。

JS的几条基本规范

  1. 不要在同一行声明多个变量
  2. 请使用*=/!*来比较true/false或者数值
  3. 使用对象字面量替代new Array这种形式
  4. 不要使用全局变量
  5. Switch语句必须带有default分支
  6. 函数不应该有时候有返回值,有时候没有返回值
  7. For循环必须使用大括号
  8. IF语句必须使用大括号
  9. for-in循环中的变量 应该使用var关键字明确限定作用域,从而避免作用域污染

var、let、const的区别

letES6 新添加申明变量的命令,它类似于 var,但是有以下不同:

  • var 声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象
  • let 声明的变量,其作用域为该语句所在的代码块内,不存在变量提升
  • const声明的变量不允许修改

null和undefined的区别

Undefined类型只有一个值,即undefined。当声明的变量还未被初始化时,变量的默认值为undefined。用法:

  • 变量被声明了,但没有赋值时,就等于undefined。
  • 调用函数时,应该提供的参数没有提供,该参数等于undefined。
  • 对象没有赋值的属性,该属性的值为undefined。
  • 函数没有返回值时,默认返回undefined。

Null类型也只有一个值,即null。null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。用法

  • 作为函数的参数,表示该函数的参数不是对象。
  • 作为对象原型链的终点。

JS有哪些内置对象

Object是JavaScript中所有对象的父对象

数据封装对象:Object、Array、Boolean、Number和String

其他对象:Function、Arguments、Math、Date、RegExp、Error

变量对象

变量对象,是执行上下文中的一部分,可以抽象为一种 数据作用域,其实也可以理解为就是一个简单的对象,它存储着该执行上下文中的所有 变量和函数声明(不包含函数表达式)

活动对象 (AO): 当变量对象所处的上下文为 active EC 时,称为活动对象。

js作用域的理解

作用域其实可理解为该上下文中声明的 变量和声明的作用范围。可分为 块级作用域函数作用域。是指代码执行时的上下文,它定义了变量以及函数生效的范围。因为JavaScript属于静态作用域链,它声明的作用域是根据程序正文在编译时就确定的,有时也称为作用域。

全局作用域的代码在程序的任何地方都能被访问到,window对象的内置属性都拥有全局作用域。

函数作用域中定义的变量,只能在函数中调用,外界无法访问。没有块级作用域导致了if或for这样的逻辑语句中定义的变量可以被外界访问,因此ES6中新增了let和const命令来进行块级作用域的声明。

作用域最大的用处就是隔离变量,不同作用域下同名变量不会产生冲突。

一般情况下,变量取值到 创建 这个变量 的函数作用域中取值。但是如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链

作用域链其本质是JavaScript在执行过程中会创造可执行上下文,可执行上下文中的词法环境中含有外部词法环境的引用,我们可以通过这个引用获取外部词法环境的变量、声明等,这些引用串联起来一直指向全局的词法环境,因此形成了作用域链

特性:

  • 声明提前: 一个声明在函数体内都是可见的, 函数优先于变量
  • 非匿名自执行函数,函数变量为 只读 状态,无法修改

闭包的理解

闭包属于一种特殊的作用域,称为 静态作用域。它的定义可以理解为: 父函数被销毁 的情况下,返回出的子函数的[[scope]]中仍然保留着父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。

简单来说闭包就是在函数里面声明函数,本质上说就是在函数内部和函数外部搭建起一座桥梁,使得子函数可以访问父函数中所有的局部变量,但是反之不可以,这只是闭包的作用之一,另一个作用,则是保护变量不受外界污染,使其一直存在内存中。在工作中我们还是少使用闭包的好,因为闭包太消耗内存,不到万不得已的时候尽量不使用。

闭包会产生一个很经典的问题:

  • 多个子函数的[[scope]]都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。

解决:

  • 变量可以通过 函数参数的形式 传入,避免使用默认的[[scope]]向上查找
  • 使用setTimeout包裹,通过第三个参数传入
  • 使用 块级作用域,让变量成为自己上下文的属性,避免共享

闭包的好处:能够实现封装和缓存等

在性能考量方面,如果不是某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能有负面影响。例如,在创建新对象或者类时,方法通常应该关联对象的原型,而不是定义到对象的构造器中,原因是这将导致每次构造器被调用时,方法都会被重新赋值(也就是说,对于每个对象的创建,方法都会被重新赋值)。

原型

构造函数是一种特殊的方法,主要用来创建对象时初始化对象。每个对象都有一个prototype原型属性,这个prototype属性是一个指针,指向一个对象,这个对象的用途是用来包含特定类型的所有实例的共享属性和方法,即这个原型对象是用来给实例对象共享属性和方法的。然后,每个实例的 _proto_ 都指向这个构造函数或者类的原型属性。

JavaScript中的对象都有一个特殊的 prototype 内置属性,其实就是对其他对象的引用。几乎所有的对象在创建时 prototype 属性都会被赋予一个非空的值,我们可以把这个属性当作一个备用的仓库,当试图引用对象的属性时会出发get操作,第一步时检查对象本身是否有这个属性,如果有就使用它,没有就去原型中查找。一层层向上直到Object.prototype顶层。

原型链

原型链是在原型的继承中出现的一个过程,我们会通过实例化一个构造函数得到一个实例化对象,当我们访问实例化对象的属性时会触发get方法,它会先在自身的属性中查找,如果没有找到这个属性,就会去_proto_中查找,一层层向上直到查找到顶层对象object。这个查找的过程我们就又称为原型链。

创建对象的几种方法

// 字面量
var o1 = {name:'o1'};
var o2 = new Object({name:'01'});
// 通过构造函数
var m = function(){this.name='01'}
var m1 = new m();
// Object.create
var p = {name:'o3'};
var o3 = Object.create(p);

继承

继承通常指的便是 原型链继承,也就是通过指定原型,并可以通过原型链继承原型上的属性或者方法。

  1. 原型链继承
  2. 借用构造函数继承
  3. 组合继承
  4. 原型式继承
  5. 寄生式继承
  6. 寄生组合式继承

es6中新增的class和extends语法,用来定义类和实现继承,底层采用了寄生组合式继承。

call、apply、bind区别

这三种方法可以改变函数调用时this的指向,区别在于函数调用的时候。

Call会立即调用函数,并要求你按次序一个个传入参数。Apply也会立即调用函数,不过需要以数组的形式传参。Call和apply效果几乎是相同的,他们都可以用来调用对象中的某个方法,具体怎么使用取决于使用场景。

三个函数的作用都是将函数绑定到上下文中,用来改变函数中this的指向;三者的不同点在于语法的不同。

const Snow = {surename: 'Snow'}
const char = {
surename: 'Stark',
knows: function(arg, name) {
 console.log(`You know ${arg}, ${name} ${this.surename}`);
}
}
char.knows('something', 'Bran');              // You know something, Bran Stark
char.knows.call(Snow, 'nothing', 'Jon');      // You know nothing, Jon Snow
char.knows.apply(Snow, ['nothing', 'Jon']);   // You know nothing, Jon Snow
注意:在es6中可以只用展开操作符方法对call进行传参
char.knows.call(Snow, ...["nothing", "Jon"]);  // You know nothing, Jon Snow 

bind不会直接触发某个方法,而是根据你传入的参数和上下文返回一个新的方法。当你想要在程序之后的某些上下文环境中调用一个方法时可以使用bind这种方式。

const Snow = {surename: 'Snow'}
const char = {
surename: 'Stark',
knows: function(arg, name) {
 console.log(`You know ${arg}, ${name} ${this.surename}`);}
}
const whoKnowsNothing = char.knows.bind(Snow, 'nothing');
whoKnowsNothing('Jon');  // You know nothing, Jon Snow 

fun.call(thisArg[, arg1[, arg2[, ...]]])

fun.apply(thisArg, [argsArray])

所以applycall的区别是call方法接受的是若干个参数列表,而apply接收的是一个包含多个参数的数组。

而bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

var bindFn = fun.bind(thisArg[, arg1[, arg2[, ...]]])

bindFn()

this的指向问题

  • 属性事件的this,在标签内调用事件函数,谁调用this所在的函数,那么this就指向谁
  • 直接在fn函数中写this(如果直接在fn函数中写this,那么this为将根据其上下文来决定,一般指向window
  • onclick事件中的this

对this对象的理解

This的指向不是在编写时确定的,而是在执行时确定的,同时,this不同的指向在于遵循了一定的规则。

首先,在默认情况下,this是指向全局对象的,比如在浏览器就是指向window。

如果函数被调用的位置存在上下文对象时,那么函数是被隐式绑定的。

其次,显示改变this指向,常见的方法就是call、apply、bind。

最后也就是优先级最高的绑定new绑定。用new调用一个构造函数,会创建一个新对象,在创造这个新对象的过程中,新对象会自动绑定到对象的this上,那么this自然就指向这个新对象。

还有一个就是箭头函数绑定,this的绑定指向就近的上下文作用域,可以理解为就近原则的指向。

函数改变this指向

由于 JS 的设计原理: 在函数中,可以引用运行环境中的变量。因此就需要一个机制来让我们可以在函数体内部获取当前的运行环境,这便是this

因此要明白 this 指向,其实就是要搞清楚 函数的运行环境,说人话就是,谁调用了函数。例如:

  • obj.fn(),便是 obj 调用了函数,既函数中的 this === obj
  • fn(),这里可以看成 window.fn(),因此 this === window

但这种机制并不完全能满足我们的业务需求,因此提供了三种方式可以手动修改 this 的指向:

  • call: fn.call(target, 1, 2)
  • apply: fn.apply(target, [1, 2])
  • bind: fn.bind(target)(1,2)

== 和 ===的区别

  • ==, 两边值类型不同的时候,要先进行类型转换,再比较
  • ===,不做类型转换,类型不同的一定不等。

==类型转换过程:

  1. 如果类型不同,进行类型转换
  2. 判断比较的是否是 null 或者是 undefined, 如果是, 返回 true .
  3. 判断两者类型是否为 string 和 number, 如果是, 将字符串转换成 number
  4. 判断其中一方是否为 boolean, 如果是, 将 boolean 转为 number 再进行判断
  5. 判断其中一方是否为 object 且另一方为 string、number 或者 symbol , 如果是, 将 object 转为原始类型再进行判断

经典面试题:[] == ![] 为什么是true

转化步骤:

  1. !运算符优先级最高,![]会被转为为false,因此表达式变成了:[] == false
  2. 根据上面第(4)条规则,如果有一方是boolean,就把boolean转为number,因此表达式变成了:[] == 0
  3. 根据上面第(5)条规则,把数组转为原始类型,调用数组的toString()方法,[]转为空字符串,因此表达式变成了:'' == 0
  4. 根据上面第(3)条规则,两边数据类型为string和number,把空字符串转为0,因此表达式变成了:0 == 0
  5. 两边数据类型相同,0==0为true

instanceof原理

能在实例的 原型对象链 中找到该构造函数的prototype属性所指向的 原型对象,就返回true。即:

// __proto__: 代表原型对象链
instance.[__proto__...] === instance.constructor.prototype// return true

代码的复用

当你发现任何代码开始写第二遍时,就要开始考虑如何复用。一般有以下的方式:

  • 函数封装
  • 继承
  • 复制extend
  • 混入mixin
  • 借用apply/call

深拷贝和浅拷贝

深拷贝利用JSON.parse(JSON.stringify())来实现深拷贝的目的,但利用JSON拷贝也是有缺点的,当要拷贝的数据中含有undefined、function、symbol类型是无法进行拷贝的,当然我们项目开发中需要深拷贝的数据一般不会含有以上三种类型,如有需要可以自己封装一个函数来实现。

浅拷贝通过es6新特性Object.assign()或者通过扩展运算符...来达到浅拷贝的目的,浅拷贝修改副本,不会影响原数据,但缺点是浅拷贝只能拷贝第一层的数据,且都是值类型数据,如果有引用类型的数据,修改副本会影响原数据。

防抖和节流

防抖与节流函数是一种最常用的 高频触发优化方式,能对性能有较大的帮助。

  • 防抖 (debounce) : 将多次高频操作优化为只在最后一次执行,通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可。

    function debounce(fn, wait, immediate) {
        let timer = null
    ​
        return function() {
            let args = arguments
            let context = this
    ​
            if (immediate && !timer) {
                fn.apply(context, args)
            }
    ​
            if (timer) clearTimeout(timer)
            timer = setTimeout(() => {
                fn.apply(context, args)
            }, wait)
        }
    }
    
  • 节流(throttle) : 每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms执行一次即可。

    function throttle(fn, wait, immediate) {
        let timer = null
        let callNow = immediate
    ​
        return function() {
            let context = this,
                args = arguments
    ​
            if (callNow) {
                fn.apply(context, args)
                callNow = false
            }
    ​
            if (!timer) {
                timer = setTimeout(() => {
                    fn.apply(context, args)
                    timer = null
                }, wait)
            }
        }
    }
    

实现原理:

防抖函数debounce指的是某个函数在某段时间内,无论触发了多少次回调,都只执行一次。假如我们设置了一个等待时间3秒的函数,在这个3秒内如果遇到函数调用请求就重新计时3秒,直到新的3秒内没有函数调用请求,此时执行函数,不然就以此类推重新计时。

实现原理就是利用定时器,函数第一次执行时设定一个定时器,之后调用时发现已经设定过定时器就清空之前的定时器,并重新设定一个新的定时器,如果存在没有被清空的定时器,当定时器计时结束后触发函数执行。

判断:

  • instanceOf
  • Constructor
  • Object.prototype.toString.call()

cookie、sessionStorage和localStorage

cookie用来保存登录信息,大小限制为4KB左右

localStorage是Html5新增的,用于本地数据存储,保存的数据没有过期时间,一般浏览器大小限制在5MB

sessionStorage接口方法和localStorage类似,但保存的数据的只会在当前会话中保存下来,页面关闭后会被清空。

名称生命期大小限制与服务器通信
cookie一般由服务器生成,可设置失效时间。如果在浏览器端生成Cookie,默认是关闭浏览器后失效4KB每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题
localStorage除非被清除,否则永久保存5MB仅在浏览器中保存,不与服务器通信
sessionStorage仅在当前会话下有效,关闭页面或浏览器后被清除5MB仅在浏览器中保存,不与服务器通信

登录:

前端本地登录,localStorege保存user信息、token(expire、token_id、token_code)、role状态。前端用户信息不加密,公司内设有大型数据库服务器,使用的是内网防范和后端算法加密。用户数据加密可以使用MD5加密或者des,保障后端日志不会记录明文密码,不会造成泄露。

LocalStorege:方法存储没有时间限制,永久性的。

sessionStorege:方法针对一个session进行数据存储。用户关闭浏览器窗口,数据会被删除。

早期的web网站是一种处理静态资源的网站,实现web应用技术的核心http协议是一个无状态的协议,它能满足早期的通讯方式。但当互联网普及,网站成为软件后,状态的保持就是一个很重要的功能。因此web应用开发里出现了保持http链接状态的技术:一个是cookie,另一个是session。

Cookie具体指的是一段小信息,它是服务器端发送出来存储在浏览器上的一组组键值对,下次访问服务器时浏览器会自动携带这些键值对,以便服务器提取有用信息。一般我们都利用cookie跟踪统计用户访问该网站的习惯,也就是说Cookie是把用户的数据写给用户的浏览器的。

Cookie虽然在一定程度上解决了“保持状态”的需求,但是它最大支持4096字节,而且保存在客户端,意味着它会被拦截和窃取。于是我们产生了新的需求,要求它能支持更多的字节,并且它保存在服务器,还要有较高的安全性,这就是Session。默认情况下一个浏览器独占一个session对象,而且session对象由服务器创建,开发人员可以调用request对象的getSession方法得到session对象。——前端调用session的用法,仅仅是sessionStorege而已,因为session是在后端服务器创建的。

0.1+0.2 != 0.3怎么处理

把需要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,等计算完成后再进行降级(除以10的n次幂),即:

(0.1*10 + 0.2*10)/10 == 0.3 //true

数组去重

第一种:通过es6的新特性set()方法

let newarr = [...new Set(arr)];

第二种:封装函数利用{}和[]

function unique(arr){
 if(!arr instanceof Array){
     throw Error('当前传入的不是数组!')
 }
 let list = [],obj={};
 arr.forEach(item=>{
     if(!obj[item]){
         list.push(item)
         obj[item]=true;
     }
 })
 return list
}

set、map解构

es6的set数据结构,它类似数组,但是成员的值都是唯一的,没有重复的值。set本身是一个构造函数,用来生成set数据结构

而map也是数据结构,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当做键。

数组排序

数组排序有两种方法:sort方法和冒泡排序

es6的新特性

  1. es6引入严格模式

    变量必须声明后在使用 函数的参数不能有同名属性, 否则报错 不能使用with语句 (说实话我基本没用过) 不能对只读属性赋值, 否则报错 不能使用前缀0表示八进制数,否则报错 (说实话我基本没用过) 不能删除不可删除的数据, 否则报错 不能删除变量delete prop, 会报错, 只能删除属性delete global[prop] eval不会在它的外层作用域引入变量 eval和arguments不能被重新赋值 arguments不会自动反映函数参数的变化 不能使用arguments.caller (说实话我基本没用过) 不能使用arguments.callee (说实话我基本没用过) 禁止this指向全局对象 不能使用fn.caller和fn.arguments获取函数调用的堆栈 (说实话我基本没用过) 增加了保留字(比如protected、static和interface)

  2. 关于let和const新增的变量声明

  3. 变量的解构赋值

  4. 字符串的扩展

    includes():返回布尔值,表示是否找到了参数字符串。 startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。 endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

  5. 数值的扩展

    Number.isFinite()用来检查一个数值是否为有限的(finite)。 Number.isNaN()用来检查一个值是否为NaN。

  6. 函数的扩展: 函数参数指定默认值

  7. 数组的扩展:扩展运算符

  8. 对象的扩展:对象的解构

  9. 新增symbol数据类型

  10. Set 和 Map 数据结构

    ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。 Set 本身是一个构造函数,用来生成 Set 数据结构。   
    

    Map它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

  11. Proxy

    Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问 都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。 Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。 Vue3.0使用了proxy。

  12. Promise

    Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理更强大:因为它解决了之前在请求中回调请求产生的回调地狱,使得现在的代码更加合理更加优雅,也更加容易定位查找问题。

    特点是:对象的状态不受外界影响。 一旦状态确定,就不会再变,任何时候都可以得到这个结果。

  13. async 函数

    async函数对 Generator 函数的区别: (1)内置执行器。 Generator 函数的执行必须靠执行器,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。 (2)更好的语义。 async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。 (3)正常情况下,await命令后面是一个 Promise 对象。如果不是,会被转成一个立即resolve的 Promise 对象。 (4)返回值是 Promise。 async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

  14. Class

    class跟let、const一样:不存在变量提升、不能重复声明... ES6 的class可以看作只是一个语法糖,它的绝大部分功能 ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

  15. Module

    ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"。 import和export命令以及export和export default的区别

解决异步回调地狱

Promise、generator、async/await

es6 module和commonjs的区别

  1. es6 module静态引入,编译时引入
  2. commonjs动态引入,执行时引入
  3. 只有es6 module才能静态分析,实现tree-shaking

什么是并发/并行

  • 并发是指一个处理器同时处理多个任务。
  • 并行是指多个处理器或者是多核处理器同时处理多个不同的任务。
  • 并发是逻辑上的同时发生,并行是物理上的同时发生。
  • 并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头。

垃圾回收:1,引用计数垃圾收集;2,标记-清除。

Web Worker

现代浏览器为JavaScript创造的 多线程环境。可以新建并将部分任务分配到worker线程并行运行,两个线程可 独立运行,互不干扰,可通过自带的 消息机制 相互通信。

基本用法:

// 创建 worker
const worker = new Worker('work.js');
​
// 向 worker 线程推送消息
worker.postMessage('Hello World');
​
// 监听 worker 线程发送过来的消息
worker.onmessage = function (event) {
  console.log('Received message ' + event.data);

限制:

  • 同源限制
  • 无法使用 document / window / alert / confirm
  • 无法加载本地资源

dom事件

1、dom事件的级别;2、dom事件模型;3、dom事件流;4、描述dom事件捕获的具体流程;5、Event对象的常用应用;6、自定义事件。

dom事件类,事件级别:

DOM0 element.onclick = function(){}
​
DOM2 element.addEventListener('click',function(){}, false)
​
DOM3 element.addEventListener('keyup',function(){}, false)

dom事件模型:捕获和冒泡

事件流:浏览器在为这个当前页面与用户做交互的过程中,点击鼠标传到页面上。

事件流:window对象,捕获目标元素,然后冒泡,window对象。

描述dom事件捕获的具体流程:window-document-html-body-目标元素 (document.documentElement)

event对象的常用:

  • event.preventDefault()阻止默认行为;
  • event.stopPropagation()阻止冒泡行为;
  • event.stopImmediatePropagation();
  • event.currentTarget当前绑定的事件;
  • event.target

变量提升hoisting

在编译过程中,JavaScript会自动把var和function声明移动到顶部的行为被称为hoisting。函数声明会被完整提升,在编写代码时可以在声明一个函数之前就调用它。但是变量只会被部分提升,例如var的声明,而赋值不会被提升,let和const也不会被提升。

函数表达式和函数声明

函数表达式只有被执行之后才可以调用,他不会被提升,相当于赋值函数表达式给变量。而函数声明则可以在定义前后被任意调用,因为它最终会被提升。

V8垃圾回收机制

垃圾回收: 将内存中不再使用的数据进行清理,释放出内存空间。V8 将内存分成 新生代空间老生代空间

  • 新生代空间: 用于存活较短的对象

    • 又分成两个空间: from 空间 与 to 空间

    • Scavenge GC算法: 当 from 空间被占满时,启动 GC 算法

      • 存活的对象从 from space 转移到 to space
      • 清空 from space
      • from space 与 to space 互换
      • 完成一次新生代GC
  • 老生代空间: 用于存活时间较长的对象

    • 从 新生代空间 转移到 老生代空间 的条件

      • 经历过一次以上 Scavenge GC 的对象
      • 当 to space 体积超过25%s
    • 标记清除算法: 标记存活的对象,未被标记的则被释放

      • 增量标记: 小模块标记,在代码执行间隙执,GC 会影响性能
      • 并发标记(最新技术): 不阻塞 js 执行
    • 压缩算法: 将内存中清除后导致的碎片化对象往内存堆的一端移动,解决 内存的碎片化

怎么理解js中的内存泄露

内存泄漏的定义是程序不需要的内存,由于某些原因其不会返回到操作系统或者可用内存池中,内存泄露会导致运行缓慢,高延迟,崩溃的问题。常见的导致内存泄露的原因有,意外的全局变量,被遗忘的计时器或回调函数,脱离文档的dom的引用。

  • 意外的全局变量: 无法被回收
  • 定时器: 未被正确关闭,导致所引用的外部变量无法被释放
  • 事件监听: 没有正确销毁 (低版本浏览器可能出现)
  • 闭包: 会导致父级中的变量无法被释放
  • dom 引用: dom 元素被删除时,内存中的引用未被正确清空

可用 chrome 中的 timeline 进行内存标记,可视化查看内存的变化情况,找出异常点。

组件化和模块化

组件化

为什么要组件化开发

有时候页面代码量太大,逻辑太多或者同一个功能组件在许多页面均有使用,维护起来相当复杂,这个时候,就需要组件化开发来进行功能拆分、组件封装,已达到组件通用性,增强代码可读性,维护成本也能大大降低

组件化开发的优点

很大程度上降低系统各个功能的耦合性,并且提高了功能内部的聚合性。这对前端工程化及降低代码的维护来说,是有很大的好处的,耦合性的降低,提高了系统的伸展性,降低了开发的复杂度,提升开发效率,降低开发成本

组件化开发的原则

  • 专一
  • 可配置性
  • 标准性
  • 复用性
  • 可维护性

模块化

为什么要模块化

早期的javascript版本没有块级作用域、没有类、没有包、也没有模块,这样会带来一些问题,如复用、依赖、冲突、代码组织混乱等,随着前端的膨胀,模块化显得非常迫切

模块化的好处

  • 避免变量污染,命名冲突
  • 提高代码复用率
  • 提高了可维护性
  • 方便依赖关系管理

模块化的几种方法

  • 函数封装
  • 立即执行函数表达式(IIFE)

mouseover和mouseenter的区别

mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是mouseout

mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave

forEach、for in、for of三者区别

forEach更多的用来遍历数

  • for in 一般常用来遍历对象或json
  • for of数组对象都可以遍历,遍历对象需要通过和Object.keys()
  • for in循环出的是key,for of循环出的是value

使用箭头函数应注意什么?

  1. 用了箭头函数,this就不是指向window,而是父级(指向是可变的)
  2. 不能够使用arguments对象
  3. 不能用作构造函数,这就是说不能够使用new命令,否则会抛出一个错误
  4. 不可以使用yield命令,因此箭头函数不能用作 Generator 函数

setTimeout、promise、async/await的区别

事件循环中分为宏任务队列和微任务队列,其中setTimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行。Promise.then里的回调函数会放到相应宏任务队列里,等宏任务队列里面的同步代码执行完再执行。Async函数表示函数里面可能会有异步方法,await后面跟一个表达式。Async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。

Promise有几种状态,什么时候会进入catch

三个状态pending、fulfilled、reject,两个过程pending->fulfilled、pending->rejected。当pending为rejected时会进入catch。

// 下面输出结果是多少
const promise = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
    console.log(2);
})
promise.then(() => {
    console.log(3);
})
console.log(4);   // 1 2 4 3

Promise 新建后立即执行,所以会先输出 1,2,而 Promise.then() 内部的代码在 当次 事件循环的 结尾 立刻执行 ,所以会继续输出4,最后输出3

使用解构赋值,实现两个变量的值的交换

let a = 1;let b = 2;
[a,b] = [b,a];

设计一个对象,键名的类型至少包含一个symbol类型,并且实现遍历所有key

let name = Symbol('name');
 let product = {
    [name]:"洗衣机",    
    "price":799
  };
  Reflect.ownKeys(product);

下面* Set结构,打印出的**size*值是多少

let s = new Set();
s.add([1]);
s.add([1]);console.log(s.size);
// 答案:2
/ **   两个数组[1]并不是同一个值,它们分别定义的数组,在内存中分别对应着不同的存储地址,因此并不是相同的值都能存储到Set结构中,所以size为2。*/

Promise中reject和catch处理上有什么区别

Reject用来抛出异常,catch用来处理异常。Reject是promise的方法,而catch是promise实例的方法。Reject后的东西,一定会进入then中的第二个回调,如果then中没有写第二个回调,则进入catch。网络异常时,比如断网就会直接进入catch而不会进入then的第二个回调。

使用* class* 手写一个* promise*

 //创建一个Promise的类
  class Promise{
    constructor(executer){//构造函数constructor里面是个执行器
      this.status = 'pending';//默认的状态 pending
      this.value = undefined//成功的值默认undefined
      this.reason = undefined//失败的值默认undefined
      //状态只有在pending时候才能改变
      let resolveFn = value =>{
        //判断只有等待时才能resolve成功
        if(this.status == pending){
          this.status = 'resolve';
          this.value = value;
        }
      }
      //判断只有等待时才能reject失败
      let rejectFn = reason =>{
        if(this.status == pending){
          this.status = 'reject';
          this.reason = reason;
        }
      }    
      try{
        //把resolve和reject两个函数传给执行器executer
        executer(resolve,reject);
      }catch(e){
        reject(e);//失败的话进catch
      }
    }
    then(onFufilled,onReject){
          //如果状态成功调用onFufilled
          if(this.status = 'resolve'){
            onFufilled(this.value);
     }
      //如果状态失败调用onReject
      if(this.status = 'reject'){
        onReject(this.reason);
      }
    }
  } 

Async/await是什么

Async函数,就是generator函数的语法糖,它建立在promise上,并且与所有现有的基于promise的api兼容。

如在一个函数前面加了async的函数,在调用函数并打印它的时候,可以发现它输出的是一个promise对象,可以说这个函数的本质就是返回一个promise对象。在这个函数里,我们再加上await后,即使调用的是异步代码,它也会变成类似于同步的代码,会让异步代码先执行,然后才执行下面的同步代码,这就是await的本质。

Async它声明在一个异步函数,(async function someName() {…}),它自动将常规函数转换成promise,返回值也是一个promise对象。只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数,异步函数内部可以使用await。

Await是暂停异步的功能执行(var result = await someAsyncCal() {…}),它放置在promise调用之前,await强制其他代码等待,直到promise完成并返回结果。而且它只能在async函数内部使用,并且只能与promise一起使用,不适用于回调。

需要注意的是,使用async/await的时候,是无法捕获错误的,这个时候就要用到es5里被遗忘的try/catch语句,来进行错误的捕获:

async testAsync() { 
    try { 
        await getJSON() 
    } catch(err) { 
    console.log(err) 
    } 
 // ...剩下的代码 
}

Async函数在声明形式上和普通函数没有区别,函数声明式,函数表达式,对象方法,class方法和箭头函数都可以声明async函数。还有就是,任何一个await语句后面的promise对象变为reject状态,整个async函数都会中断执行。因为async函数返回的是pomise对象,必须等到内部所有await命令后面的promise对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法。

Await/async相比于promise的优势

代码读起来更加同步,promise虽然摆脱了回调地狱,但是then的链式调用也会带来额外的阅读负担。Promise传递中间值非常麻烦,而async/await几乎是同步的写法,非常优雅。并且错误处理友好,它们可以用es5里成熟的tray/catcch语句,而promise的错误捕获非常冗余。

调试友好,promise的调试很差,由于没有代码块,你不能在一个返回表达式的箭头函数中设置断点,如果你在一个.then代码块中使用调试器的步进(step-over)功能,调试器并不会进入后续的.then代码块,因为调试器只能跟踪同步代码的【每一步】。

JavaScript异步编程回顾

由于JavaScript是单线程执行模型,因此必须支持异步编程才能提高运行效率。异步编程的语法目标是让异步过程写起来像同步过程。

回调函数,就是把任务的第二段单独写一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。回调函数最大的问题是容易形成回调地狱,即多个回调函数嵌套,降低代码可读性,增加逻辑的复杂性,容易出错。

Promise,是为解决回调函数的不足,社区创造出promise。Promise实际上是利用编程技巧将回调函数改成链式调用,避免回调地狱。它也有问题,就是代码冗余,原来的任务被promise包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。

为了解决promise问题,async/await在es7中被提了出来,是目前为止最好的解决方案。Async/await函数写起来跟同步函数一样,条件是需要接受promise或原始类型的值。异步编程的最终目标是转换成人类最容易理解的形式。

因为async是generator的语法糖,而generator函数是协程在es6的实现。协程简单来说就是多个线程互相协作,完成异步任务 。大概的过程是,第一步,协程A开始执行。第二步,协程A执行到一半,进入暂停,执行权转移到协程B。第三步,(一段时间后)协程B交还执行权。第四步,协程A恢复执行。

整个generator函数就是一个封装的异步任务,异步操作需要暂停的地方,都用yield语句注明。

function* gen(x) {
console.log('start')
const y = yield x * 2
return y
}
​
const g = gen(1)
g.next()   // start { value: 2, done: false }
g.next(4)  // { value: 4, done: true }

Json和xml的区别

  • 数据体积方面,Xml是重量级的,json是轻量级的,传递的速度更快些。
  • 数据传输方面,xml在传输过程中比较占带宽,json占带宽少,易于压缩。
  • 数据交互方面,json与JavaScript的交互更加方便,更容易解析处理,更好的进行数据交互。
  • 数据描述方面,josn对数据的描述性比xml较差。
  • Xml和json都用在项目交互下,xml多用于做配置文件,json用于数据交互。