前端面试常见问题——JS篇

1. 闭包

  • 闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在函数内创建函数,通过函数访问函数的局部变量,利用闭包可以突破作用链域
  • 特性:
    • 函数内再嵌套函数
    • 内部函数可以引用外层的参数和变量
    • 参数和变量不会被垃圾回收机制回收
  • 优缺点
    • 优点是可以避免全局变量的污染,设计私有的方法和变量来实现封装
    • 缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露,所以不能滥用闭包,退出函数之前,将不使用的局部变量全部删除。
  • 作用
    • 让函数外部可以操作函数内部的数据
    • 让这些变量始终保持在内存中,延长局部变量生命周期
    • 封装对象的私有属性和私有方法
2. 作用域链
  • 作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的
  • 简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期
  • 全局作用域 编写在script标签中的js代码,都在全局作用域。全局作用域在页面打开时创建,在页面关闭时销毁。在全局作用域中有一个全局对象window,代表浏览器窗口,由浏览器创建,可以直接使用。我们创建的全局变量会作为window对象的属性保存,创建的方法都会作为window对象的方法保存
  • 函数作用域 调用函数时创建,函数执行完毕后销毁,每调用一次函数就会创建一个函数作用域,相互独立。在函数作用域中可以访问到全局作用域的变量,在全局作用域中不能访问到函数作用域的变量。当在函数作用域中操作某个变量时,先在自身作用域中寻找这个变量,没有的话再向上一级作用域找,直到找到全局作用域(也就是顺着作用域链找)如果在函数作用域里有和全局作用域里同名的变量,但是要访问全局作用域里的变量,可通过window对象调用
  • 块级作用域 一般是通过let声明和const声明产生的变量,在所属的块的作用域外不能访问。块通常是一个被花括号包含的代码块。优点是使外部作用域不能访问内部作用域的变量,规避命名冲突
3. 原型链
  • 每个对象都会在其内部初始化一个属性,就是__proto__,当我们访问一个对象的属性时如果这个对象内部不存在这个属性,那么他就会去__proto__里找这个属性,这个__proto__又会有自己的__proto__,于是就这样一直找下去,也就是我们平时所说的原型链的概念
  • 关系
    • instance.constructor.prototype = instance.__proto__
  • 特点
    • JavaScript对象通过引用传递,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变
  • 当我们需要一个属性时,Javascript引擎会先看当前对象中是否有这个属性,如果没有就会查找他的原型对象是否有这个属性,如此递推下去,一直检索到 Object 内建对象
4. 继承
  • 父类
// 父类 function Superclass(name) { this.name = name; this.say = function() { alert(this.name); } } Superclass.prototype.age = 10;

  • 原型链继承
让新实例的原型等于父类的实例,可以方便的继承父类型的原型中的方法
缺点:无法向父类构造函数传参;所有新实例都会共享父类实例的属性,即原型上的属性共享,一个实例修改了原型属性,另一个实例的原型属性也会被修改,属性的继承无意义
// 原型链继承 function Subclass() { this.name= "sub name"; } Subclass.prototype = new Superclass(); const sub = new Subclass();

  • 借用构造函数继承
.call().apply()将父类构造函数引入子类函数,等于复制父类的实例属性给子类;创建子类实例时,可以向父类传递参数,可以实现多继承,可以方便的继承父类型的属性,但是无法继承原型中的方法
缺点:只能继承父类构造函数的属性,方法在构造函数中定义,函数复用无从谈起;超类型原型中定义的方法对子类型不可见。实例并不是父类的实例,只是子类的实例
// 借用构造函数继承 function Subclass(name) { Superclass.call(this, name); this.age = 12; } const sub = new Subclass("sub name");

  • 组合继承
【前端面试常见问题——JS篇】结合原型链继承和借用构造函数继承两种模式的优点:传参与复用,既通过在原型上定义方法实现函数复用,又能保证每个实例都有自己的属性;既是子类的实例,也是父类的实例
缺点:调用两次父类构造函数,在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费
// 组合原型链继承和借用构造函数继承 function Subclass(name) { Superclass.call(this, name); // 借用构造函数继承 } Subclass,prototype = new Superclass(); // 原型链继承 const sub = new Subclass("sub name");

  • ES6 Class通过 extends 关键字实现继承
class 实现继承的核心在于使用 extends 表明继承自哪个父类,并在子类构造函数中必须调用 super
class Parent { constructor(value) { this.val = value; } getValue() { console.log(this.val); } } class Child extends Parent { constructor(value) { super(value); this.val = value; } } const child = new Child(1); child.getValue() // 1 child instanceof Parent // true

  • 原型式继承
在函数内部创建一个临时性构造函数,将传入的对象作为构造函数的原型,最终返回临时构造函数的一个实例
// 封装一个容器,用于输出对象和承载继承的原型 function content(obj) { function F() {} F.prototype = obj; // 继承传入的原型 return new F(); // 返回对象 } const sup = new Superclass(); // 获取父类实例 const sub = content(sup);

  • 寄生式继承
function content(obj) { function F() {} F.prototype = obj; // 继承传入的原型 return new F(); // 返回对象 } const sup = new Superclass(); // 获取父类实例 // 以上为原型式继承, 给原型式继承再套一个壳子来传递参数 function subobject(obj) { const sub = content(obj); sub.name = "sub name"; return sub; } var sub = subobject(sup);

  • 寄生组合式继承
// 寄生组合式继承,解决了组合式继承调用两次构造函数的缺点 function content(obj) { function F() {} F.prototype = obj; // 继承传入的原型 return new F(); // 返回对象 } //con实例的原型继承了父类的原型 var con = content(Superclass.prototype); function Subclass(name) { Superclass.call(this, name); // 借用构造函数继承,继承父类构造函数的属性 } Subclass.prototype = con; con.constructor = Subclass; // 修复实例 const sub = new Subclass();

5. ES5和ES6继承的区别
  • ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上Parent.apply(this)
  • ES6的继承机制实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this
  • ES5的继承时通过原型或构造函数机制来实现。
  • ES6通过class关键字定义类,有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。
  • super() 与 Parent.call(this) 是不同的,在继承原生构造函数的情况下,体现得很明显,ES6 中的子类实例可以继承原生构造函数实例的内部属性,而在 ES5 中做不到。ES5 中 A.call(this) 中的 this 是构造函数 B 的实例,也就是在实现实例属性继承上,ES5 是先创造构造函数 B 的实例,然后在让这个实例通过 A.call(this) 实现实例属性继承,在 ES6 中,是先新建父类的实例对象this,然后再用子类的构造函数修饰 this,使得父类的所有行为都可以继承。
    作者:T_superlu
    链接:https://juejin.im/post/6844903924015120397
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
6. this对象
  • this总是指向函数的执行上下文,根据调用方式不同,this的指向不同。
  • 以匿名函数或直接调用的函数;this指向全局上下文(浏览器window NodeJS global);
  • 以方法调用则this指向调用方法的那个对象;
  • 以构造函数调用时,this指向新创建的对象;
  • 使用callapply调用时this为指定的那个对象;
  • 在事件响应函数中,指向触发这个事件的对象,特殊的是IE中的attachEvent中的this总是指向全局对象window
  • 箭头函数中this的指向取决于箭头函数声明的位置,捕获其所在的上下文的this值,作为自己的this值
7. call、apply、bind
  • call
函数实例的方法,指定该函数内部this的指向,在所指定的作用域中立即执行该函数。传递两个参数。第一个参数是指定函数内部中this的指向,第二个参数是函数调用时需要传递的参数。实现如下:
  • 首先context为可选参数,如果不传的话默认上下文为window
  • context创建一个fn属性,并将值设置为需要调用的函数
  • 因为call可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
  • 然后调用函数并将对象上的函数删除
Function.prototype.myCall = function (context) { const context = context || window; conetxt.fn = this; // 取出context后面的参数 const args = [...arguments].slice(1); const result = context.fn(...args); // 删除 fn delete context.fn; return result; }

  • apply
apply方法与call方法类似,也是改变this指向,然后在指定的作用域中立即调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数。实现如下:
Function.prototype.myApply = function (context) { const context = context || window; conetxt.fn = this; // 判断是否存在第二个参数,如果存在则将第二个参数展开 const result = arguments[1] ? context.fn(...arguments[1]) : context.fn(); // 删除 fn delete context.fn; return result; }

  • bind
bind方法用于指定函数内部的this指向(执行时所在的作用域),然后返回一个新函数。bind方法并非立即执行一个函数,需要再调用一次才行。实现如下:
  • bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式
  • 对于直接调用,选择了apply的方式实现,但是对于参数需要注意:bind可以实现类似f.bind(obj, 1)(2)的代码,所以需要将两边的参数拼接起来,于是就有了args.concat(...arguments)
  • 对于new的情况来说,不会被任何方式改变this,所以对于这种情况需要忽略传入的this
Function.prototype.myBind = function (context) { const _this = this; constargs = [...arguments].slice(1); // 返回一个函数F return function F() { // 判断是否为 new F() if (this instanceof F) { return new _this(...args, ...arguments); } return _this.apply(context, args.concat(...arguments)); } }

  • 联系与区别
    • 第一个参数都是指定函数内部中this的指向(函数执行时所在的作用域),然后根据指定的作用域,调用该函数
    • 都可以在函数调用时传递参数。call,bind方法需要直接传入,而apply方法需要以数组的形式传入
    • call,apply方法是在调用之后立即执行函数,而bind方法没有立即执行,需要将函数再执行一遍
8. 事件模型(事件流)及各个阶段
  • 事件流三阶段:捕获阶段——>目标阶段——>冒泡阶段
  • document ——> target目标 ——> document
  • document.documentElement获取html标签;document.body获取body标签
  • 事件流分为捕获事件流和冒泡事件流
    • 捕获事件流从根节点开始执行,向子节点查找执行,直到执行到目标节点
    • 冒泡事件流从目标节点开始执行,向父节点冒泡查找执行,直到根节点。
    • 事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个目标节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行
  • addEventListener的第三个参数设置为
    • true表示该元素在事件的“捕获阶段”(由外往内传递时)响应事件
    • false表示该元素在事件的“冒泡阶段”(由内向外传递时)响应事件
  • 阻止冒泡
    • W3C中,使用stopPropagation()方法
    • IE下设置cancelBubble = true
  • 阻止事件的默认行为
    • W3C中,使用preventDefault()方法
    • IE下设置window.event.returnValue = https://www.it610.com/article/false
9. 事件代理(事件委托)
  • 事件代理Event Delegation,又称事件委托。是常用绑定事件的常用技巧,即是把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。
  • 原理:DOM元素的事件冒泡
event对象里记录有“事件源”,它就是发生事件的子元素。存在兼容性问题,在老的IE下,事件源是 window.event.srcElement,其他浏览器是 event.target。绑定事件的元素为event.currentTarget
  • 好处是可以大量节省内存占用,减少事件注册,提高性能,实现当新增子对象时无需再次对其绑定
10. 事件循环
  • 首先,JS是单线程的,主要的任务是处理用户的交互,使用事件队列的形式,
  • JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 task队列中。一旦执行栈为空,Event Loop 就会从 task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为
  • JS中分为两种任务类型
    • macrotask宏任务,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
      每一个task会从头到尾将这个任务执行完毕,不会执行其它
      浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->...)
    • microtask微任务,可以理解是在当前 task 执行结束后立即执行的任务,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)
  • 运行机制
    • 执行宏任务
    • 执行过程中将遇到的微任务添加到微任务队列中
    • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务
    • 当前宏任务执行完毕,渲染
    • 渲染完毕后,开始下一个宏任务(从事件队列中获取)
11. addEventListener()和attachEvent()的区别
  • addEventListener()是符合W3C规范的标准方法; attachEvent()是IE低版本的非标准方法
  • addEventListener()支持事件冒泡和事件捕获; 而attachEvent()只支持事件冒泡
  • addEventListener()的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 false,决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性:
    • capture布尔值,和useCapture作用一样
    • once布尔值,值为true表示该回调只会调用一次,调用后会移除监听
    • passive布尔值,表示永远不会调用preventDefault
  • addEventListener()的第一个参数中,事件类型不需要添加on; attachEvent()需要添加'on'
  • 如果为同一个元素绑定多个事件, addEventListener()会按照事件绑定的顺序依次执行, attachEvent()会按照事件绑定的顺序倒序执行
12. 自定义事件
  • 事件的创建
var myEvent = new CustomEvent('event_name', { detail: { // 将需要传递的数据写在detail中,以便在EventListener中通过event.detail中得到 } });

  • 事件的监听
window.addEventListener('event_name', function(event) { console.log(event.detail); });

  • 事件的触发
自定义的事件由于不是JS内置的事件,所以我们需要在JS代码中去显式地触发它。方法是使用window.dispatchEvent触发;IE8低版本兼容,使用window.fireEvent
if(window.dispatchEvent) { window.dispatchEvent(myEvent); } else { window.fireEvent(myEvent); }

13. new操作符
  • 创建一个空对象,链接原型,绑定this指向
  • 属性和方法被加入到 this 引用的对象中,即执行构造函数
  • 返回新创建的对象
14. 定义对象的方法
  • 对象字面量: var obj = {};
  • 构造函数: var obj = new Object();
  • Object.create(): var obj = Object.create(Object.prototype);
  • 工厂模式:无法复用
  • 原型模式:原型上的属性共享
  • 组合模式(原型+构造函数)
15. 判断对象类型
  • 通过 Object.prototype.toString.call(xx)可以获得类似 [object Type] 的字符串。(不能精准判断自定义对象,对于自定义对象只会返回[object Object])
  • instanceof可以正确的判断对象的类型,但对原始类型不可以
  • constructor除了undefinednull,其他类型的变量均能使用constructor判断出类型,但constructor属性是可以被修改的,会导致检测出的结果不正确
16. typeof与instanceof
  • typeof 对于原始类型来说,除了 null 都可以显示正确的类型
  • typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型
  • 如果我们想判断一个对象的正确类型,这时候可以考虑使用 `instanceof``,但不能判断原始类型
  • instanceof的原理及手动实现
// 表达式:A instanceof B // 如果B的显式原型对象在A对象的原型链上,返回true function myInstanceof(left, right) { let prototype = right.prototype; left = left._proto_; while(true) { if(left === null || left === undefined) return false; if(prototype === left) return true; left = left._proto_; } }

17. 类型转换 https://www.cnblogs.com/Juphy/p/7085197.html
  • 显式转换
    • Number(object)
      boolean:true/false ——>1/0
      nubmer:返回本身
      null:0
      undefined:NaN
      string:纯数字转换为十进制,忽略前导0;有效浮点格式转换为浮点数值,忽略前导0;空字符串转换为0;其他转换为NaN
      object:调用对象valueOf()方法,依据上面的转换规则转换返回的值,若转换结果为NaN,则调用对象toString()方法,再依据上面的转换规则转换字符串值

      前端面试常见问题——JS篇
      文章图片
      对象valueOf()的返回值
    • parseInt(string,radix)
      将字符串转换为整数类型的数值(非字符串先转换为字符串)
      (1)忽略字符串前面的空格,直至找到第一个非空字符
      (2)如果第一个字符不是数字符号或者负号,返回NaN
      (3)如果第一个字符是数字,则继续解析直至字符串解析完毕或者遇到一个非数字符号为止
      (4) 如果上步解析的结果以0开头,则将其当作八进制来解析; 如果以0x开头,则将其当作十六进制来解析
      (5)如果指定radix参数,则以radix为基数进行解析
    • parseFloat(string)
      规则与parseInt基本相同,区别在于字符串中第一个小数点符号是有效的,会忽略所有前导0,若包含一个可解析为整数的数,则返回整数而不是浮点数
    • toString()
      除undefined和null之外的所有类型的值都具有toString()方法,其作用是返回对象的字符串表示

      前端面试常见问题——JS篇
      文章图片
      toString()
    • String(mix)
      有toString()方法则调用
      null返回"null"; undefined返回"undefined"
    • Boolean(mix)
      除false、空字符串、NaN、0、null以及undefined外都为true
  • 隐式转换
    • 递增递减操作符、一元正负符号操作符
      与Number规则基本相同
    • 加法运算操作符
      有一个操作值为字符串,则将另一个操作值转换为字符串,最后连接起来
    • 逻辑操作符( !、&&、|| )
      逻辑非( ! )操作符首先通过Boolean()函数将操作值转换为布尔值,然后求反
      逻辑与( && )操作符,如果一个操作值不是布尔值时,遵循以下规则:
      (1) 如果第一个操作数经Boolean()转换后为true,则返回第二个操作值,否则返回第一个操作值
      (2) 如果有一个操作值为null / NaN / undefined,返回null / NaN / undefined
      逻辑或( || )操作符,如果一个操作值不是布尔值,遵循以下规则:
      (1) 如果第一个操作值经Boolean()转换后为false,则返回第二个操作值,否则返回第一个操作值
      (2) 对于undefinednullNaN的处理规则与逻辑与( && )相同
    • 关系操作符( <, >, <=, >= )
      (1) 如果两个操作值都是数值,则进行数值比较
      (2) 如果两个操作值都是字符串,则比较字符串对应的字符编码值
      (3) 如果只有一个操作值是数值,则将另一个操作值转换为数值,进行数值比较
      (4) 如果一个操作数是对象,则调用valueOf()方法(如果对象没有valueOf()方法则调用toString()方法),得到的结果按照前面的规则执行比较
      (5)如果一个操作值是布尔值,则将其转换为数值,再进行比较
      注:NaN是非常特殊的值,它不和任何类型的值相等,包括它自己,同时它与任何类型的值比较大小时都返回false。
    • 相等操作符( == )
      (1) 如果一个操作值为布尔值,则先将其转换为数值
      (2) 如果一个操作值为字符串,另一个操作值为数值,则通过Number()函数将字符串转换为数值
      (3) 如果一个操作值是对象,另一个不是,则调用对象的valueOf()方法,得到的结果按照前面的规则进行比较
      (4) null与undefined是相等的
      (5) 如果一个操作值为NaN,则相等比较返回false
      (6) 如果两个操作值都是对象,则比较它们是不是指向同一个对象
18. Ajax原理
  • Ajax的原理简单来说是在用户和服务器之间加了—个中间层(AJAX引擎),通过XMLHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用javascript来操作DOM更新页面。使用户操作与服务器响应异步化。这其中最关键的一步就是从服务器获得请求数据
  • Ajax的过程只涉及JavaScriptXMLHttpRequestDOMXMLHttpRequestajax的核心机制
var xhr = null; // 创建XMLHttpRequest对象 if(window.XMLHttpRequest) { xhr = new XMLHttpRequest(); // IE7+/Firefox/Chrome/Opera/Safari } else { xhr = new ActiveXObject("Microsoft.XMLHTTP"); } // open(method, url, async) // - method 请求类型; GET或POST // - url 请求地址 // - async 是否异步 xhr.open('get', url, true); // send(string) 将请求发送至服务器 // - string 仅用于POST请求 xhr.send(null); // 当readyState发生改变时会触发onreadystatechange 事件 xhr.onreadystatechange = function() { //readyState存有XMLHTTPRequest的状态 // 0: 请求未初始化 // 1: 服务器连接已建立 // 2: 请求已接收 // 3: 请求处理中 // 4: 请求已完成,且响应已就绪 if(xhr.readyState == 4) { // 200 "OK" if(xhr.status == 200) { // responseText 获得字符串形式的响应数据 // responseXML 获得XML形式的响应数据 console.log(xhr.responseText); } else { console.log(xhr.status) } } }

  • 优点:
    • 通过异步模式,提升了用户体验
    • 优化浏览器和服务器之间的传输,减少不必要的数据往返,减少带宽占用
    • Ajax在客户端运行,承担了一部分本来由服务器承担的工作,减少了大用户量下的服务器负载
    • 实现动态局部刷新
  • 缺点:
    • 安全问题,AJAX暴露了与服务器交互的细节
    • 对搜索引擎的支持比较弱
    • 不容易调试
    • 在动态更新页面的情况下,用户无法回到前一个页面状态
19. 同源策略与跨域问题
  • 同源策略指的是:协议,域名,端口相同,同源策略是一种安全协议,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击
  • 对于浏览器而言只要域名、协议、端口其中一个不同就会引发同源策略,从而限制他们之间如下的交互行为:
    • Cookie、LocalStorage和IndexDB无法读取;
    • DOM无法获得;
    • AJAX请求不能发送。
  • 解决跨域问题
    • 跨域资源共享(CORS Cross-Origin Resource Sharing)
      定义了在访问跨域资源时,浏览器与服务器应该如何沟通。CORS背后的基本思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。服务器端对于CORS的支持,主要就是通过设置Access-Control-Allow-Origin来进行的。如果浏览器检测到相应的设置,就可以允许Ajax进行跨域的访问
    • JSONP 即JSON with Padding(填充式json)
      在js中,直接用XMLHttpRequest请求不同域上的数据时,是不可以的。但在页面上引入不同域上的js脚本文件却是可以的,jsonp正是利用这个特性来实现。 例如:a.html页面需要利用ajax获取一个不同域上的json数据,假设这个json数据地址是http://example.com/data.php,那么a.html中的代码就可以这样:

js文件载入成功后会执行我们在url参数中指定的函数,并且会把我们需要的json数据作为参数传入。所以jsonp是需要服务器端的页面进行相应的配合的。(即用JavaScript动态加载一个script文件,同时定义一个callback函数给script执行)
优点:不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果。
缺点:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。
CORS与JSONP相比,无疑更为先进、方便和可靠。
(1)JSONP只能实现GET请求,CORS支持所有类型的HTTP请求;
(2)使用CORS,开发者可以使用普通的XMLHttpRequest发起请求和获得数据,比起JSONP有更好的错误处理;
(3)JSONP主要被老的浏览器支持,它们往往不支持CORS,而绝大多数现代浏览器都已经支持了CORS;
  • 修改document.domain来跨子域
    如果是需要处理 Cookie 和 iframe 该怎么办呢?这时候就可以通过修改document.domain来跨子域。两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain共享 Cookie或者处理iframe。比如A网页是http://w1.example.com/a.html,B网页是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享Cookie。LocalStorage 和 IndexDB 无法通过这种方法规避同源策略。服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如.example.com
  • HTML5的window.postMessage
    HTML5为了解决跨域通信问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。
    这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。
    a页面向b页面发消息调用postMessage方法, b页面通过监听message事件可以接受到来自a页面的消息
// a页面document.getElementById('frame1').onload = function() { var win = document.getElementById( 'frame1 ').contentwindow; win.postMessage("我是来自a页面的", "http://127.0.0.1/JSONP/b.html") } // b页面 window.onmessage = function(e) { e = e ll event; console.log(e.data); //我是来自a页面的 }

  • WebSocket进行跨域
    web sockets是一种浏览器的API,它的目标是在一个单独的持久连接上提供全双工、双向通信。
    原理:在js创建了web socket之后,会有一个HTTP请求发送到浏览器以发起连接。取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议交换为web sockt协议。只有在支持web socket协议的服务器上才能正常工作。
var socket = new websockt( 'ws: //www.baidu.com'); socket.send( "hello websockt" ); socket.onmessage = function(event) { var data = https://www.it610.com/article/event.data; }

20. 异步加载JS(延迟加载) JavaScript默认是同步加载(又称阻塞模式),这种模式在加载js文件时,会影响后续页面的渲染,一旦网速不好,整个页面将会等待js文件的加载,从而不进行后续页面的渲染。
另外,有些js文件是按需加载的,用的时候加载,不用时不必加载。所以引入了异步加载模式(非阻塞模式),即浏览器在下载执行js文件时,会同时进行后续页面的处理。
  • 动态添加script标签
这样js脚本都会在onload事件后才加载,onload事件会在所有文件内容(包括文本、图片、CSS文件等)加载完成后才开始执行,极大的优化了网页的加载速度,提高了用户体验
function loadScript(url, callback){ var s = document.createElement('script'); s.type = 'text/javascript'; if(s.readyState){ s.onreadystatechange = function(){//兼容IE if(s.readyState == 'complete' || s.readyState == 'loaded'){ callback(); } } }else{ s.onload = function(){//safari chrome opera firefox callback(); } } s.src = https://www.it610.com/article/url; document.head.appendChild(s); }

  • defer/async
    • defer不会阻塞dom树构建,立即异步加载。加载好后,如果 dom 树还没构建好,则先等 dom 树解析好再执行;如果 dom 树已经准备好,则立即执行
    • async不会阻塞dom树构建,立即异步加载,加载好后立即执行
    • deferasync在网络读取(下载)方面都是异步的(相较于 HTML 解析),区别在于脚本下载完之后何时执行,并且defer按照加载顺序执行脚本,而async脚本的加载和执行是紧紧挨着的,所以不管声明的顺序如何,只要加载完了就会立刻执行
      前端面试常见问题——JS篇
      文章图片
21. 造成内存泄漏的操作
  • 意外的全局变量引起的内存泄漏:js中如果不用 var 声明变量,该变量将被视为 window 对象(全局对象)的属性,也就是全局变量.
  • 闭包引起的内存泄漏
  • 没有清理的dom元素引用
  • 被遗忘的定时器或者回调
22. JSON
  • JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,基于JavaScript的一个子集。数据格式简单, 易于读写, 占用带宽小
  • JSON字符串转换为JSON对象
var obj = eval('(' + str + ')'); var obj = str.parseJSON; var obj = JSON.parse(str);

  • JSON对象转换为JSON字符串
var str = obj.toJSONString(); var str = JSON.stringify(obj);

23. XML和JSON的区别
  • 数据体积:JSON相对于XML来讲,数据的体积小。
  • 数据交互:JSONJavaScript的交互更加方便,更容易解析处理
  • 数据描述:JSON对数据的描述性比XML较差
  • 传输速度:JSON的速度要远远快于XML
24. JS设计模式 https://www.cnblogs.com/tugenhua0707/p/5198407.html#_labe1
  • 工厂模式(解决多个相似的问题)
function createPerson (name, age, sex){ var obj= new Object() ; obj.name = name; obj.age = age; obj.sex = sex; obj.sayName = function() { return this.name; } return obj; }

当然工厂模式并不仅仅是用来 new 出实例。假设有一份很复杂的代码需要用户去调用,用户只负责传递需要的参数,至于参数的使用,内部的逻辑是不关心的,只需要最后返回一个实例。这个构造过程就是工厂。作用就是隐藏了创建实例的复杂度,只需要提供一个接口,简单清晰。
  • 单体模式
单体模式是一个用来划分命名空间并将一批属性和方法组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次。优点是:
1.可以用来划分命名空间,减少全局变量的数量。
2.使用单体模式可以使代码组织的更为一致,使代码容易阅读和维护。
3.可以被实例化,且实例化一次。
单例模式的核心就是保证全局只有一个对象可以访问,只需要用一个变量确保实例只创建一次
var singleton = function (name) { this.name =name ; }; singleton.prototype.getName = function() { return this.name; } // 获取实例对象 var getinstance = ( function () { var instance = null; return function (name) { if( !instance) { instance = new singleton (name) ; } return instance; } })();

  • 发布者-订阅者模式
定义了对象间的一种一对多的关系,当一个对象发生改变时,所有依赖于它的对象都将得到通知
优点:支持广播通信,发布者与订阅者耦合度低
缺点:创建订阅者消耗时间和内存;代码不易理解和维护
var shoeobj = {}; // 定义发布者 shoeobj.list = []; // 缓存列表存放订阅者回调函数 //增加订阅者 shoeobj.listen = function (key,fn) { if(!this.list[key]){ // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表 this.list[key] = []; } this.list[key].push(fn); // 订阅消息添加到缓存列表 } // 发布消息 shoeobj.trigger = function () { var key = Array.prototype.shift.call(arguments); // 取出消息类型名称 var fns = this.list[key]; // 取出该消息对应的回调函数的集合 //如果没有订阅过该消息的话,则返回 if(!fns || fns.length === 0) return; for(let i = 0; i < fns.length; i++){ fns[i].apply(this,arguments); //arguments是发布消息时附送的参数 } };

  • 代理模式
代理是为了控制对对象的访问,不让外部直接访问到对象,事件代理就是很好的例子
  • 外观模式`
外观模式提供了一个接口,隐藏了内部的逻辑,更加方便外部调用。如实现一个兼容多种浏览器的添加事件方法。
function addEvent(elm, evType, fn, useCapture){ if (elm.addEventListener){ elm.addEventListener(evType, fn, useCapture); return true; } else if (elm.attachEvent ){ var r = elm.attachEvent("on" +evType, fn); return r; } else { elm[ "on" +evType] = fn; } }

25. offsetWidth/offsetHeight; clientWidth/clientHeight与scrollWidth/scrollHeight的区别
offsetWidth // width + padding + border-width offsetHeight // height + padding + border-width clientWidth // width + padding clientHeight // height + padding scrollWidth // width + padding + 溢出尺寸,无溢出时为盒子的clientWidth scrollHeight // height + padding + 溢出尺寸,无溢出时为盒子的clientHeightoffsetTop // 返回元素的上外缘距离最近采用定位父元素内壁的距离,如果父元素中没有采用定位的,则是获取上外边缘距离文档内壁的距离 //其中所谓的定位就是position属性值为relative、absolute或者fixed。返回值是一个整数,单位是像素。此属性是只读的 offsetLeft // 和offsetTop的原理是一样的,只不过方位不同 scrolTop // 获取或者设置对象的最顶部到对象在当前窗口显示的范围内的顶边的距离,也就是元素滚动条被向下拉动的距离 //返回值是一个整数,单位是像素。此属性是可读写的 scrollLeft // 此属性可以获取或者设置对象的最左边到对象在当前窗口显示的范围内的左边的距离,也就是元素被滚动条向左拉动的距离// 当鼠标事件发生时(不管是onclick,还是omousemove,onmouseover等) clientX // 鼠标相对于浏览器(浏览器的有效区域)左上角x轴的坐标; 不随滚动条滚动而改变; clientY // 鼠标相对于浏览器(浏览器的有效区域)左上角y轴的坐标; 不随滚动条滚动而改变; pageX // 鼠标相对于浏览器(浏览器的有效区域)左上角x轴的坐标; 随滚动条滚动而改变; pageY // 鼠标相对于浏览器(浏览器的有效区域)左上角y轴的坐标; 随滚动条滚动而改变; screenx // 鼠标相对于显示器屏幕左上角x轴的坐标; screenY // 鼠标相对于显示器屏幕左上角y轴的坐标; offsetX // 鼠标相对于事件源左上角X轴的坐标 offsetY // 鼠标相对于事件源左上角Y轴的坐标

26. JS数据类型
  • 基本数据类型:UndefinedNullBooleanNumberString
  • 引用数据类型:对象、数组和函数
  • 两种类型的区别是:存储位置不同;
  • 基本数据类型直接存储在栈stack中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型存储在堆heap中的对象,占据空间大、大小不固定,如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体
27. null,undefined 的区别
  • undefined :是一个表示"无"的原始值或者说表示"缺少值",就是此处应该有一个值,但是还没有定义,当尝试读取时会返回 undefined。例如变量被声明了,但没有赋值时,就等于undefined
  • null : 是一个对象(空对象, 没有任何属性和方法)
  • 在验证null时,一定要使用 === ,因为 ==无法分别null 和 undefined(null == undefined 为 true)
    在JavaScript规范中提到,要比较相等性之前,不能将nullundefined转换成其他任何值,并且规定nullundefined是相等的,都代表无效的值,全等于的状态下为false,因为不属于同一数据类型
typeof null // object typeof undefined // undefined

28. Js内置对象
  • ObjectJavaScript 中所有对象的父对象
  • 数据封装类对象:ObjectArrayBooleanNumberString
  • 其他对象:FunctionArgumentsMathDateRegExpError
29. 常见兼容性问题
  • 标准的事件绑定方法函数为addEventListener,IE下是attachEvent
  • W3C标准规定,事件是作为函数的参数传入的,IE采用了一种非标准的方式,将事件作为window对象的event属性
  • ajax的实现方式不同,获取XMLHttpRequest的不同,IE(5/6)下是activeXObject
  • 不同浏览器的标签默认的外补丁margin和内补丁padding不同 解决方案: css 里增加通配符 * { margin: 0; padding: 0; }
  • 获得DOM节点的父节点、子节点的方式不同:其他浏览器:parentNode/parentNode.childNodes;IE:parentElement/parentElement.children
  • 当标签的高度设置小于10px,在IE6、IE7中会超出自己设置的高度; 解决方案:超出高度的标签设置overflow:hidden,或者设置line-height的值小于你的设置高度
  • cursor:hand 显示手型在safari 上不支持; 解决方案:统一使用 cursor:pointer
30. eval
  • 功能是把对应的字符串解析成JS代码并运行
  • 应该避免使用eval,不安全,非常耗性能(2次,一次解析成js语句,一次执行)
  • JSON字符串转换为JSON对象的时候可以用eval
var obj =eval('('+ str +')');

31. "use strict"; 是什么意思
  • use strict是一种ECMAscript 5 添加的严格运行模式,这种模式使得 Javascript 在更严格的条件下运行,使JS编码更加规范化的模式,消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为,消除代码运行的一些不安全之处,保证代码运行的安全;提高编译器效率,增加运行速度;为未来新版本的Javascript做好铺垫。
  • 指令use strict
    直接写在中的第一行,表示该篇js都处于严格模式下
    写在方法中的第一行,表示该方法下的代码格式都处于严格模式下
  • 严格模式的限制:
    • 不允许使用未声明的变量
    • 不允许删除变量、对象、函数,只能删除configurable设置为true的对象属性
    • 不允许变量重名
    • 不允许使用八进制
    • 不允许使用转义字符
    • 不允许对只读属性赋值
    • 不允许对一个使用getter方法读取的属性进行赋值
    • 不允许删除一个不允许删除的属性
    • 变量名不能使用 "eval" 、"arguments"字符串
    • 不允许使用with语句
    • 在作用域 eval() 创建的变量不能被调用
    • 禁止this关键字指向全局对象
    • 新增保留关键字:let、public、private、static等
32. 数组去重 1. 利用ES6 Set去重(ES6中最常用)
Array.from(new Set(arr)); [...new Set(arr)];

2. 利用for嵌套for,然后splice去重(ES5中最常用)
function unique(arr) { for(let i = 0; i < arr.length; i++) { for(let j = i + 1; j < arr.length; j++) { if(arr[i] == arr[j]) { // 第一个等同于第二个,splice方法删除第二个 arr.splice(j,1); j--; } } } return arr; }

3. 利用indexOf/includes去重
function unique(arr) { var array =[]; for (let i = 0; i < arr.length; i++) { if (array.indexof(arr[i]) === -1) { // 也可以用 array.includes(arr[i]) 作为判断条件 array .push(arr[i]) } } return array; }

4. 利用sort()
function unique(arr) { arr = arr. sort(); var arrry = [arr[0]]; for (let i = 1; i < arr.length; i++){ if (arr[i] !== arr[i-1]) { arrry.push(arr[i]); } } return arrry; }

5. 利用filter
function unique(arr) { return arr.filter(function(item, index, arr) { return arr.indexof(item,0) === index; }); }

6. 利用递归去重
function unique(arr) { var array= arr; var len = array.length; array.sort(); function loop(index){ if(index >= 1) { if(array[index] === array[index-1]){ array.splice(index,1); } loop(index - 1); //递归loop,然后数组去重 } } loop(len-1); return array; }

7. 利用reduce+includes
function unique(arr) { return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]); }

33. 数组降维
  • 数组字符串化
var arr = [[222,333,444,], [55,66,77],{a:1}]; arr += ''; arr = arr.split(','); console.log(arr); // [222,333,444,55,66,77,[object Object]]

  • 递归
function reduceDimension(arr) { let ret = []; let toArr = function(arr) { arr.forEach((item) => { item instanceof Array ? toArr(item) : ret.push(item); }); } toArr(arr); return ret; }

  • Array.prototype.flat()
var arr1 = [1, 2, [3, 4]]; arr1.flat(); // [1, 2, 3, 4] var arr2 = [1, 2, [3, 4, [5, 6]]]; arr2.flat(); // [1, 2, 3, 4, [5, 6]] var arr3 = [1, 2, [3, 4, [5, 6]]]; arr3.flat(2); // [1, 2, 3, 4, 5, 6] //使用工nfinity作为深度,展开任意深度的嵌套数组 arr3.flat(Infinity); // [1, 2, 3, 4,5, 6]

var arr1 = [1,2,3,[1,2,3,4,[2,3,4]]]; var res; // 不使用递归,使用stack反嵌套多层嵌套数组 function flatten(input) { const stack = [...input]; const res = []; while (stack.length) { // 使用pop 从 stack中取出并移除值 const next = stack.pop(); if(Array.isArray(next)) { // 使用push送回内层数组中的元素 stack.push(...next); } else { res.push(next); } } //使用reverse恢复原数组的顺序 return res.reverse(); } res = flatten(arr1); console.log(res); // [1,2,3,1,2,3,4,2,3,4]

34. 数组/对象遍历
  • 数组遍历
  • for循环
    最简单的一种循环遍历方法,也是使用频率最高的一种,使用临时变量缓存长度可优化性能
var arr = [1, 2, 3, 4, 5, 6]; var len = arr.length; for(var i = 0; i < len; i++) { console.log(arr[i]); } // 1 2 3 4 5 6

  • for in
    主要是用来循环遍历对象的属性,用来遍历数组效率最低(输出的 key 是数组索引)
var arr = ['我', '是', '谁', '我', '在', '哪']; for(var key in arr) { console.log(key); } // 0 1 2 3 4 5

  • for of(ES6)
    性能要好于 for…in…,但仍然比不上普通的for 循环(不能循环对象)
var arr = ['我', '是', '谁', '我', '在', '哪']; for(var key of arr) { console.log(key); } // 我 是 谁 我 在 哪

  • foreach
    数组自带的遍历方法,使用频率略高,但是性能仍然比普通循环略低
var arr = [1, 2, 3, 4, 5, 6]; arr.forEach(function (item, idnex, array) { console.log(item); // 1 2 3 4 5 6 console.log(array); // [1, 2, 3, 4, 5, 6] })

  • map
    遍历每一个元素并且返回对应的元素(可以返回处理后的元素) ,返回的新数组和旧数组的长度是一样的,使用比较广泛,但其性能差于forEach
var arr = [1, 2, 3, 4, 5, 6]; var newArr = arr.map(function (item, idnex) { return item * item; }) console.log(newArr); // [1, 4, 9, 16, 25, 36]

  • 对象遍历
  • for in
    主要用于遍历对象的可枚举属性,包括自有属性、继承自原型的属性,可以搭配hasOwnProperty方法筛选自有属性
  • Object.keys
    主要用于遍历对象自有的可枚举属性,不包括继承自原型的属性和不可枚举的属性
  • Object.getOwnProperty
    主要用于返回对象的自有属性,包括可枚举和不可枚举的属性,不包括继承自原型的属性
35. map与forEach的区别
  • 相同点
    • 都是循环遍历数组中的每一项
    • 每次执行匿名函数都支持三个参数,参数分别为item(当前每一项),index(索引值),arr(原数组)
    • 匿名函数中的this都是指向window
    • 只能遍历数组
  • 不同点
    • map()会分配内存空间存储新数组并返回,而不会改变原数组的值,所以在callback需要有return值,如果没有会返回undefined
    • forEach()允许callback更改原始数组的元素
36. 将函数参数arguments转为数组
  • 传统方法:遍历arguments,将元素添加至新数组中
  • Array.prototype.slice.call(arguments);
37. 面向对象编程
  • 基本思想:使用对象,类,继承,封装等基本概念来进行程序设计
  • 优点:易维护、易扩展、重用性降低重复工作量、缩短开发周期
  • 与面向过程编程的区别
    • 面向过程编程是分析出解决问题所需要的步骤,然后用函数把这些步骤实现,使用的时候依次调用
    • 面向对象编程是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为,以功能来划分问题,而不是步骤
38. 函数柯里化
  • 在一个函数中,先填充几个参数,然后再返回一个新的函数的技术,称为函数的柯里化。通常用于在不侵入函数的前提下,为函数预置通用参数,供多次重复调用。
  • bind可用于实现函数柯里化
39. Js动画与CSS动画区别及相应实现
  • CSS3的动画的优点
    • 在性能上会稍微好一些,浏览器会对CSS3的动画做一些优化
    • 代码相对简单
  • 缺点
    • 在动画控制上不够灵活
    • 兼容性不好
  • JavaScript的动画正好弥补了这两个缺点,控制能力很强,可以单帧的控制、变换,同时写得好完全可以兼容IE6,并且功能强大
  • 对于一些复杂控制的动画,使用javascript会比较靠谱。而在实现一些小的交互动效的时候,就多考虑考虑CSS
40. callee和caller的作用
  • caller javascript函数类型的一个属性,它引用调用当前函数的函数;如果在javascript程序中,函数是由顶层调用的,则返回null
  • callee 函数上下文中arguments对象的属性,引用的是函数自身
41. BOM
  • 浏览器对象模型,提供一组对象来完成对浏览器的操作,包括
    • Window代表浏览器窗口,同时也是网页中的全局对象
    • Navigator代表当前浏览器信息,通过该对象可识别不同的浏览器
    • Location代表当前浏览器的地址栏信息,可以获取地址栏信息或操作浏览器跳转页面
    • History代表浏览器的历史记录,可通过该对象操作浏览器历史记录,不能获取具体历史记录,只能操作向前向后翻页且只在当次访问时有效
    • Screen代表用户屏幕信息,屏幕宽高及分辨率等信息
  • BOM对象在浏览器中都作为window对象的属性保存,可通过window对象调用,也可直接使用
  • Navigator
    • userAgent含有用来描述浏览器信息的内容
    • 如果不能通过userAgent不能判断,还可以通过一些浏览器中的特有对象来判断(主要因为IE11+无法再通过userAgent判断),如ActiveXObject
  • History
    • length属性获取当次访问的链接数量;
    • back()回退上一个页面;
    • forward()前进下一个页面;
    • go()跳转指定页面,以整数作为参数,整数为向前跳转页面数,负数为向后跳转页面数
  • Location
    • 直接打印location可以获取当前页面的完整路径;
    • 修改location为一个绝对或相对路径,则页面会自动跳转并生成相应的历史记录;
    • assign()跳转至其他页面,与直接修改location的作用一样;
    • reload()重新加载当前页面;传入true为参数则强制清空缓存刷新页面;
    • replace()使用新的页面替换当前页面,但不生成历史记录;
42. 异步编程
  • 回调函数
    • 优点:简单、容易理解和部署
    • 缺点:不利于维护,代码耦合高,而且每个任务只能指定一个回调函数
  • 事件监听(采用事件驱动模式,取决于某个事件是否发生)
    • 优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数
    • 缺点:事件驱动型,流程不够清晰
  • 发布/订阅(观察者模式)
    • 类似于事件监听,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行,但是可以通过‘消息中心’,了解现在有多少发布者,多少订阅者,从而监控程序的运行,故优于事件监听模式
  • Promise对象
    • 优点:可以利用then方法,进行链式写法,流程清楚,有一整套的配套方法,可以实现许多强大的功能;如果一个任务已经完成,再添加回调函数会立即执行,不用担心是否错过了某个事件或信号;
    • 缺点:编写和理解相对比较难
  • Generator函数
    • 优点:函数体内外的数据交换、错误处理机制
    • 缺点:流程管理不方便
  • async函数
    • 优点:内置执行器、更好的语义、更广的适用性、返回的是Promise、结构清晰。
    • 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。
43. WebSocket
  • 由于 http存在一个明显的弊端,即消息只能由客户端推送到服务器端,而服务器端不能主动推送到客户端,导致如果服务器如果有连续的变化,这时只能使用轮询。而轮询效率过低,并不适合,于是 WebSocket被发明出来,相比与 http 具有以下优点
    • 支持双向通信,实时性更强;
    • 可以发送文本,也可以二进制文件;
    • 协议标识符是 ws,加密后是 wss ;
    • 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。
    • 支持扩展。
    • 无跨域问题。
    • 实现比较简单,服务端库如 socket.iows,可以很好的帮助入门。客户端也只需要参照 api 实现即可
  • 如何兼容低版本浏览器
    • Adobe Flash Socket
    • ActiveX HTMLFile (IE)
    • 基于 multipart 编码发送 XHR:原理是让客户端在一次请求中保持和服务端连接不断开,然后服务端源源不断传送数据给客户端,就好比数据流一样,并不是一次性将数据全部发给客户端
    • 长轮询
44. 防抖/节流
  • 防抖
防抖是将多次执行变为最后一次执行
/** * @desc函数防抖 * @param func函数 * @param wait延迟执行毫秒数 * @param immediate true表立即执行,false表非立即执行 */ funtion debounce(func, wait, immediate) { let timeout; return function () { let context = this; let args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { var callNow = !timeout; timeout = setTimeout(() => { timeout = null; }, wait); if (callNow) func.apply(context,args); } else { timeout = setTimeout(function() { func.apply(context,args) }, wait); } } }

  • 节流
节流是将多次执行变成每隔一段时间执行
function throttle(func, wait) { let timeout; return function() { let context - this; let args = arguments; if(!timeout) { timeout = setTimeout(() => { timeout = null; func.apply(context, args); },wait); } } }

45. 变量提升/函数提升/暂时性死区
  • 变量提升
当执行 JS 代码时,会生成执行环境,在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,变量只声明并且赋值为 undefined
console.log(a); //undefined var a = 123;

  • 函数提升
具名函数的声明有两种方式:函数声明式/函数字面量式
函数字面量式声明和普通变量一样,提升的只是一个没有值的变量
函数声明式的提升现象和变量提升略有不同,函数声明式会提升到作用域最前边,并且将声明内容一起提升到最上边
bar() //函数字面量式 var bar = function() { console.log(1); } // 报错:TypeError: bar is not a functionbar() //函数声明式 function bar() { console.log(1); } //输出结果1

  • 同名变量声明,Javascript采用的是忽略原则,后声明的会被忽略,变量声明和赋值操作可以写在
    一起,但是只有声明会被提升,提升后变量的值默认为undefined,结果是在赋值操作执行前变量的值必为undefined
  • 同名函数声明,Javascript采用的是覆盖原则,先声明的会被覆盖,因为函数在声明时会指定函数
    的内容,所以同一作用域下一系列同名函数声明的最终结果是调用时函数的内容和最后一次函数声明相同
  • 对于同名的函数声明和变量声明,采用的是忽略原则,由于在提升时函数声明会提升到变量声明之前,变量声明一定会被忽略,所以结果是函数声明有效
  • 暂时性死区
ES6规定,let/const 命令会使区块形成封闭的作用域。若在声明之前使用变量,就会报错ReferenceError。在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为 “暂时性死区”temporal dead zone TDZ
46. 执行上下文栈
  • JavaScript引擎创建了执行上下文栈来管理执行上下文。可把执行上下以文栈认为是一个存储函数调栈结构用的,遵循先进后出的原则
  • 一开始浏览器执行全局的代码时,首先创建全局执行上下文,压入执行栈顶部
  • 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
  • 浏览器的JS执行引擎总是访问栈顶的执行上下文。
  • 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
47. ajax/axios/fetch区别
  • 传统 ajax隶属于原始js,核心使用XMLHttpRequest对象,多个请求之间如果有先后关系的话,会出现回调地狱。JQuery ajax 是对原生XHR的封装。 缺点如下:
    • 本身是针对MVC的编程,不符合现在前端MVVM的浪潮
    • 基于原生的XHR开发,XHR本身的架构不清晰
    • JQuery整个项目太大,单纯使用ajax却要引入整个JQuery非常的不合理
    • 不符合关注分离(Separation of Concerns)的原则
    • 配置和调用方式非常混乱,而且基于事件的异步模型不友好
  • axios本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范,具有以下特征:
    • 从浏览器中创建 XMLHttpRequest
    • 支持 Promise API
    • 客户端支持防止CSRF
    • 提供了一些并发请求的接口(重要,方便了很多的操作)
    • 从 node.js 创建 http 请求
    • 拦截请求和响应
    • 转换请求和响应数据
    • 取消请求
    • 自动转换JSON数据
  • fetch是XMLHttpRequest的一种替代方案,而不是ajax的进一步封装,没有使用XMLHttpRequest对象,是基于Promise设计,调用简单,挂在BOM上的,属于全局的方法,很友好的解决了回调地狱问题,优点如下:
    • 语法简洁,更加语义化
    • 基于标准 Promise 实现,支持 async/await
    • 更加底层,提供的API丰富(request, response)
    • 脱离了XHR,是ES规范里新的实现方式
try { let response = await fetch(url); let data = https://www.it610.com/article/response.json(); console.log(data); } catch(e) { console.log("Oops, error", e); }

  • fetch是一个低层次的API,可以把它考虑成原生的XHR,所以使用起来并不是那么舒服,需要进行封装,且没有办法原生监测请求的进度,而XHR可以
48. Hybrid
  • Hybrid技术是一种混合开发模式,即同时使用NativeWeb搭建的App。定义是: "同时使用网页语言与程序语言开发,通过应用商店区分移动操作系统分发,用户安装使用”。总体特性更接近Native App,以Javascript + Native两者相互调用为主,从开发层面实现“一次开发,多处运行”的机制,成为真正适合跨平台的开发,兼具了Native App良好用户交互体验的优势,也兼具了Web App使用H5跨平台开发低成本的优势。
  • Hybrid App的本质其实是在原生的 App 中,使用 WebView 作为容器直接承载 Web页面。因此,最核心的点就是 Native端 与 H5端 之间的双向通讯层,即需要一套跨语言通讯方案,来完成 Native与 JavaScript 的通讯
  • JSBridge通信原理
    • Native调用JS
      webview 作为 H5 的宿主,Native 可以通过 webview 的 API直接执行 Js 代码,例如:
      ios可以通过webview的evaluateJavaScript:completionHandler方法来运行js的代码
      android可以通过webview的loadUrl()去调用js代码,也可以使用evaluateJavascript()来调用js代码
    • JS调用Native,有3种常见的方案:
      1.WebView URL Scheme 跳转拦截
      URL SCHEME 是一种类似于url的链接,是为了方便app直接互相调用设计的,形式和普通的 url 近似,主要区别是 protocol 和 host 一般是自定义的。拦截 URL SCHEME 的主要流程是:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作。
      2.WebView中的prompt/console/alert拦截
      通常使用prompt,因为这个方法在前端中使用频率低,比较不会出现冲突
      3.WebView API注入
      通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的
  • 现在比较流行的混合方案主要有三种,主要是在UI渲染机制上的不同
    • 基于 WebView UI的基础方案,通过 JSBridge 完成 H5 与 Native 的双向通讯,从而赋予H5一定程度的原生能力。
    • 基于Native UI的方案,例如 React-Native、Weex。在赋予 H5 原生API能力的基础上,进一步通过 JSBridge 将js解析成的虚拟节点树(Virtual DOM)传递到 Native 并使用原生渲染。
    • 小程序方案,也是通过更加定制化的 JSBridge,并使用双 WebView 双线程的模式隔离了JS逻辑与UI渲染,形成了特殊的开发模式,加强了 H5 与 Native 混合程度,提高了页面性能及开发体验。
以上的3种方案,其实同样都是基于 JSBridge 完成的通讯层,第2、3种方案,其实可以看做是在方案1的基础上,继续通过不同的新技术进一步提高了应用的混合程度。因此,JSBridge 也是整个混合应用最关键的部分。

    推荐阅读