又菜又牛逼Web开发Javascript收集

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

2020-09-10  本文已影响0人  Marvel_Dreamer

1. 闭包


2. 作用域链


3. 原型链


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");

结合原型链继承和借用构造函数继承两种模式的优点:传参与复用,既通过在原型上定义方法实现函数复用,又能保证每个实例都有自己的属性;既是子类的实例,也是父类的实例
缺点:调用两次父类构造函数,在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费

// 组合原型链继承和借用构造函数继承
function Subclass(name) {
  Superclass.call(this, name); // 借用构造函数继承
}
Subclass,prototype = new Superclass(); // 原型链继承
const sub = new Subclass("sub name");

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继承的区别


6. this对象


7. call、apply、bind

函数实例的方法,指定该函数内部this的指向,在所指定的作用域中立即执行该函数。传递两个参数。第一个参数是指定函数内部中this的指向,第二个参数是函数调用时需要传递的参数。实现如下:

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方法与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方法用于指定函数内部的this指向(执行时所在的作用域),然后返回一个新函数。bind方法并非立即执行一个函数,需要再调用一次才行。实现如下:

Function.prototype.myBind = function (context) {
  const _this = this;
  const  args = [...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));
  }
}

8. 事件模型(事件流)及各个阶段


9. 事件代理(事件委托)

event对象里记录有“事件源”,它就是发生事件的子元素。存在兼容性问题,在老的IE下,事件源是 window.event.srcElement,其他浏览器是 event.target。绑定事件的元素为event.currentTarget


10. 事件循环


11. 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操作符


14. 定义对象的方法


15. 判断对象类型


16. typeof与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


18. Ajax原理

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)
    }
  }
}

19. 同源策略与跨域问题

<script type="text/javascript">
  function dosomething(jsondata) {
    //处理获得的json数据
  }
</ script>
<script src="http://example.com/data.php?callback=dosomething"></script>

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;

// a页面
<iframe id="frame1" src="http://127.0.0.1/JSONP/b.html" frameborder="1"></iframe>
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页面的
}
var socket = new websockt( 'ws: //www.baidu.com');
socket.send( "hello websockt" );
socket.onmessage = function(event) {
  var data = event.data;
}

20. 异步加载JS(延迟加载)

JavaScript默认是同步加载(又称阻塞模式),这种模式在加载js文件时,会影响后续页面的渲染,一旦网速不好,整个页面将会等待js文件的加载,从而不进行后续页面的渲染。
另外,有些js文件是按需加载的,用的时候加载,不用时不必加载。所以引入了异步加载模式(非阻塞模式),即浏览器在下载执行js文件时,会同时进行后续页面的处理。

这样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 = url;
    document.head.appendChild(s);
}

21. 造成内存泄漏的操作


22. JSON

var obj = eval('(' + str + ')');
var obj = str.parseJSON;
var obj = JSON.parse(str);
var str = obj.toJSONString();
var str = JSON.stringify(obj);

23. XML和JSON的区别


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 + 溢出尺寸,无溢出时为盒子的clientHeight

offsetTop // 返回元素的上外缘距离最近采用定位父元素内壁的距离,如果父元素中没有采用定位的,则是获取上外边缘距离文档内壁的距离
//其中所谓的定位就是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数据类型


27. null,undefined 的区别

typeof null // object
typeof undefined // undefined

28. Js内置对象


29. 常见兼容性问题


30. eval

var obj =eval('('+ str +')');

31. "use strict";是什么意思


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;
}
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. 数组/对象遍历

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
var arr = ['我', '是', '谁', '我', '在', '哪'];
for(var key in arr) {
    console.log(key);
} // 0 1 2 3 4 5
var arr = ['我', '是', '谁', '我', '在', '哪'];
for(var key of arr) {
    console.log(key);
} // 我 是 谁 我 在 哪
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]
})
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]

35. map与forEach的区别


36. 将函数参数arguments转为数组


37. 面向对象编程


38. 函数柯里化


39. Js动画与CSS动画区别及相应实现


40. callee和caller的作用


41. BOM


42. 异步编程


43. WebSocket


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 function

bar()
//函数声明式
function bar() {
  console.log(1);
} //输出结果1

ES6规定,let/const 命令会使区块形成封闭的作用域。若在声明之前使用变量,就会报错ReferenceError。在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为 “暂时性死区”temporal dead zone TDZ


46. 执行上下文栈


47. ajax/axios/fetch区别

try {
  let response = await fetch(url);
  let data = response.json();
  console.log(data);
} catch(e) {
  console.log("Oops, error", e);
}

48. Hybrid

以上的3种方案,其实同样都是基于 JSBridge 完成的通讯层,第2、3种方案,其实可以看做是在方案1的基础上,继续通过不同的新技术进一步提高了应用的混合程度。因此,JSBridge 也是整个混合应用最关键的部分。

上一篇下一篇

猜你喜欢

热点阅读