前端面试总结--JS

基本数据类型

String、Number、Boolean、null、undefined

类型判断

判断基本数据类型用typeof: MDN

  • typeof 'aaa' // string
  • typeof 123 // number
  • typeof true // boolean
  • typeof null // object 因为null和object的类型标签都是0,null是一个空对象指针
  • typeof undefined // undefined
  • typeof 函数 // function
  • typeof 其他对象 // object

判断引用类型用instanceof: MDN

  • instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。
  • 用instanceof来判断基本数据类型会报错

如何判断一个数据是NaN

NaN==NaN; // false
isNaN(NaN); // true
Number.isNaN(NaN); // true
Object.is(NaN,NaN); // true 

null与undefined区别

null 表示值被定义但是个空值,null是一个空对象指针;
undefined 表示变量声明但未赋值;
参考:理解 | 堆内存栈内存释放、null和{}、undefined的区别

null == undefined // true
null === undefined // false

typeof null // object
typeof undefined // undefined

Number(null) // 0
Number(undefined) // NaN

作用域链

定义:访问一个变量时,会先在当前作用域查找该变量,若找到就直接使用,若未找到就继续向上一层查找,直到全局作用域。这种链式查询关系就是作用域链。
其实作用域链在函数定义时已经确定了,作用域链是和函数定义时的位置相关的。在函数创建的时候创建一个包含外部对象(包括全局对象和所有包含自己的对象)的作用域链,储存在内部[[scope]]属性中。函数执行的时候会创建一个执行环境,通过复制[[scope]]属性中的对象,构建执行环境的作用域链,并把自己的活动对象推向当前作用域链的前端以此形成完整的作用域链。[[scope]]属性中保存的是对可访问变量对象的引用,而不是值的复制。
参考:函数的作用域链在定义时已经确定!!

闭包

定义:闭包就是能够读取其他函数内部变量的函数。
优点:局部变量可被重用且不会被污染(变量私有化)。
缺点:由于变量不会被回收,所以滥用闭包会导致内存溢出,所以要及时释放不再需要的闭包。
代码

function count() {
    var num = 0;
    return function(){
    	num++;
    }
}
count();

闭包是在定义时确定的

前端面试总结--JS_第1张图片
前端面试总结--JS_第2张图片

参考:JS闭包的理解

事件处理机制

DOM事件流存在三个阶段:事件捕获阶段处于目标阶段事件冒泡阶段
在捕获阶段触发事件:addEventListener(event, listener, true)
在冒泡阶段出发事件:addEventListener(event, listener, false), attachEvent(event,listener)

事件委托

<ul>
	<li></li>
	<li></li>
	<li></li>
</ul>

window.onload = function(){ 
 var UL = document.getElementById('ul');
    //委托ul上的点击事件,将当前点击的li节点变为红色
    UL.onclick = function(ev){ 
     var e = ev || window.event;
      var target = e.target || window.event.srcElement;            
          //判断target是否符合要求的元素节点      
          if(target.tagName.toLowerCase() == 'li'){
                 //将当前点击这个li节点变成红色       
                 target.style.backgroundColor = 'red';
                  }   
          }
  }

如果我们需要在每一个li上绑定一个事件,就可以利用事件冒泡原理,将这些事件绑定到ul上,让ul来代为处理。
优点:

  • 提高性能。每个函数都会占用内存,使用一个事件可减少内存占用。
  • 动态监听。新增的节点无需再重新绑定事件也拥有和其他节点一样的事件。

阻止事件冒泡

W3C的方法: event.stopPropagation()
IE的方法: event.cancelBubble =true

function stopBubble(e) {
    if (e && e.stopPropagation) {
        e.stopPropagation();
    } else {
        window.event.cancelBubble = true;
    }
}

取消默认事件

W3C的方法: event.preventDefault()
IE的方法: event.returnValue =false

function stopDefault(e) {
    if (e && e.preventDefault) {
        e.preventDefault();
    } else {
        window.event.returnValue = false;
    }
}

垃圾回收机制

标记清除:垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
引用计数:当声明了一个变量并将一个引用类型值赋值该变量时,则这个值的引用次数就是1.如果同一个值又被赋给另外一个变量,则该值得引用次数加1。相反,如果包含对这个值引用的变量又取 得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那 些引用次数为零的值所占用的内存。
参考:js垃圾回收机制和引起内存泄漏的操作;JS垃圾回收机制;谈谈JS 垃圾回收机制

this的指向

方法调用模式:this指向对象。
函数调用模式:this指向window。
构造器调用模式:this指向实例。
call和apply调用:this指向传入的第一个参数。
箭头函数:this指向定义时所在的对象,call和apply失效;不可以当做构造函数;不可以使用arguments对象;不可以使用yield命令。
参考:call、apply和bind方法的用法以及区别;函数的四种调用模式及this指向;JS this指向总结

call、apply和bind的用法以及区别

它们作用都是改变函数运行时this的指向。

function func (a,b,c) {}

// call的第一个参数是要绑定给this的值,从第二个参数开始是接收的参数列表。
// 当第一个参数为null、undefined的时候,this默认指向window。
func.call(obj, 1, 2, 3)

// apply接受两个参数,第一个参数是要绑定给this的值,第二个参数是一个参数数组。
// 当第一个参数为null、undefined的时候,this默认指向window。
func.apply(obj, [1,2,3])

// bind和call很相似,第一个参数是this的指向,从第二个参数开始是接收的参数列表。
// 区别在于bind的返回值是一个改变了this指向的函数,不会立即执行,原来的函数this的指向是不变的。
func.bind(obj, 1, 2, 3)

参考:call、apply和bind方法的用法以及区别;让你弄懂 call、apply、bind的应用和区别;「干货」细说 call、apply 以及 bind 的区别和用法

原型和原型链

function SuperType(){
	this.property = true;
}
SuperType.prototype.getSuperValue = function(){
	return this.property;
}
var superInstance = new SuperType();

前端面试总结--JS_第3张图片

function SubType(){
	this.subProperty = false;
}
SubType.prototype.getSubValue = function(){
	return this.subProperty;
}
var subInstance = new SubType();

前端面试总结--JS_第4张图片

SubType.prototype = new SuperType();
SubType.prototype.getSubValue1 = function(){
	return this.subProperty;
}
var instance = new SubType();
alert(instance.getSuperValue()); //true
//查找顺序:instance→SubType.prototype→SuperType.prototype

前端面试总结--JS_第5张图片

注意:subInstance.constructor = SuperType,因为SubType.prototype指向了另一个对象,导致constructor 被重写了;同理,subInstance.getSubValue()也已经访问不到了。

如何实现继承

1-经典继承(借用构造函数)
优点:可以在子类构造函数中向父类构造函数传参;避免了引用类型的属性被所有实例共享。
缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法,无法复用。

function SuperType(){
	this.colors = ['red','blue','green'];
}
function SubType(){
	SuperType.call(this);//继承了SuperType
}
var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors);//["red", "blue", "green", "black"]
var instance2 = new SuperType();
console.log(instance2.colors);//["red", "blue", "green"]

2-原型链继承(借用原型链)
优点:方法可以复用。
缺点:引用类型的属性被所有实例共享;创建子类的实例时,不能向父类传参。

function SuperType(){
}
SuperType.prototype.name= "Nicholas";
SuperType.prototype.sayName = function(){
	console.log(this.name);
}
function SubType(){
}
SubType.prototype = new SuperType();
SubType.prototype.age = 29;
SubType.prototype.sayAge = function(){
	console.log(this.age);
}
var instance1 = new SubType();
instance1.sayName();//"Nicholas"
instance1.sayAge();//29

3-伪经典继承(组合继承)
优点:融合原型链继承和构造函数继承的优点,是 JavaScript 中最常用的继承模式。

function SuperType(name){
	this.name = name;
	this.colors = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
	console.log(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(){
	console.log(this.age);
}

var instance1 = new SubType('Nicholas',29);
instance1.colors.push('black');
console.log(instance1.colors);//["red", "blue", "green", "black"]
instance1.sayName();//"Nicholas"
instance1.sayAge();//29
var instance2 = new SubType('Greg',27);
console.log(instance2.colors);//["red", "blue", "green"]
instance2.sayName();//"Greg"
instance2.sayAge();//27

参考:js各种继承方式和优缺点的介绍

新建对象的方法

1-直接新建

let person = {
	sex: 'femail',
	age: '19',
	eat: function(){
		console.log('eating');
	}
}

2-工厂模式

function createAPerson(){
	let person = new Object();
	person.sex = 'femail';
	person.age = '19';
	person.eat = function(){
		console.log('eating');
	}
	return person;
}
let person = createAPerson();

3-构造函数模式

function Person(){
	this.sex = 'femail';
	this.age = '19';
	this.eat = function(){
		console.log('eating');
	}
}
let person = new Person();

4-原型模式

function Person(){
}
Person.prototype.sex = 'femail';
Person.prototype.age = '19';
Person.prototype.eat = function(){
	console.log('eating');
}
let person = new Person();

5-混合模式(组合使用构造函数模式和原型模式)

function Person(){
	this.sex = 'femail';
	this.age = '19';
}
Person.prototype.eat = function(){
	console.log('eating');
}
let person = new Person();

参考:js中对象与对象创建方法的各种方法

EventLoop

宏任务
宏任务的例子有很多,包括创建主文档对象、解析HTML、执行主线JavaScript代码,更改当前的URL、以及各种事件、setTimeout、setInterval等。
微任务
微任务是更小的任务,主要包括Promise的回调函数、DOM发生变化。微任务需要尽可能快的、通过异步的方式执行。
事件循环基于两个基本原则
一次处理一个任务。
一个任务开始后直到运行完成,不会被其他任务中断。
参考:Promise自我修养之事件循环
前端面试总结--JS_第6张图片

怎么区分宏任务和微任务

宏任务是由宿主发起的,而微任务由JavaScript自身发起
参考链接:宏任务和微任务到底是什么?

前端面试总结--JS_第7张图片

MutationObserver

描述:监视一个节点及其全部子节点树的添加、移除元素,以及任何属性变化的事件
应用:群组组件监听disabled属性

var targetNode = document.querySelector("#someElement");
var observerOptions = {
  childList: true,  // 观察目标子节点的变化,是否有添加或者删除
  attributes: true, // 观察属性变动
  subtree: true     // 观察后代节点,默认为 false
}
var observer = new MutationObserver(callback);
observer.observe(targetNode, observerOptions);

//回调函数
function callback(mutationList, observer) {
  mutationList.forEach((mutation) => {
    switch(mutation.type) {
      case 'childList':
        /* 从树上添加或移除一个或更多的子节点;参见 mutation.addedNodes 与
           mutation.removedNodes */
        break;
      case 'attributes':
        /* mutation.target 中某节点的一个属性值被更改;该属性名称在 mutation.attributeName 中,
           该属性之前的值为 mutation.oldValue */
        break;
    }
  });
}

变量和函数的优先级

函数提升优先级高于变量提升,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。而且存在同名函数与同名变量时,优先执行函数。
参考:JS中变量提升与函数提升及其优先级

前端面试总结--JS_第8张图片

堆内存和栈内存

前端面试总结--JS_第9张图片

参考:JavaScript栈内存和堆内存

深拷贝和浅拷贝

深拷贝和浅拷贝是针对引用数据类型的,比如数组和对象。基本数据类型不存在深浅拷贝之分
浅拷贝:只复制引用,原对象属性值改变,新的属性值也会改变。(只复值第一层
深拷贝:创建一个新的内存,复制真正的值,原对象属性值改变,新的属性值不会受影响。(复制每一层

浅拷贝数组:

// 方法1
let arr1 = [1,2,3,4,5];
let arr2 = [...arr1];

// 方法2
let arr1 = [1,2,3,4,5];
let arr2 = arr1.slice(0)

// 方法3
let arr1 = [1,2,3,4,5];
let arr2 = arr1.concat()

浅拷贝对象:

// 方法1
var o2 = Object.assign({}, o1)

// 方法2
var o2 = {...o1}

深拷贝数组和对象:

// 方法1,需要求目标对象(非 undefined,function)
const obj2 = JSON.parse(JSON.stringify(obj1));

// 方法2
function deepClone(item){
  const target = item.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
  for(let keys in item){ // 遍历目标
    if(item.hasOwnProperty(keys)){
      if(item[keys] && typeof item[keys] === 'object'){ // 如果值是对象,就递归一下
        target[keys] = item[keys].constructor === Array ? [] : {};
        target[keys] = deepClone(item[keys]);
      }else{ // 如果不是,就直接赋值
        target[keys] = item[keys];
      }
    }
  }
  return target;
}

浅拷贝内存分析
前端面试总结--JS_第10张图片
深拷贝内存分析
前端面试总结--JS_第11张图片
参考:内存分析-深浅拷贝

函数柯里化(Currying)

定义:柯里化是指通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式
优点:函数复用、延迟执行、提前确认
参考:详解JS函数柯里化;「前端面试题系列6」理解函数的柯里化

// 一个普通函数
function sum(a, b, c) {
  console.log(a + b + c);
}

// 柯里化后
const fn = curry(sum)

// 可能的调用方式
fn(1, 2, 3); // 6
fn(1, 2)(3); // 6
fn(1)(2, 3); // 6
fn(1)(2)(3); // 6

// 优点1:可以复用
// 优点2:可延迟执行
const tempF = fn(1, 2)
const res1 = tempF(3) // 6
const res2 = tempF(4) // 7

// 柯里化实现方法
function curry (fn, currArgs) {
  return function() {
  	// 将 arguments 数组化
    let args = [].slice.call(arguments);

    // 首次调用时,若未提供最后一个参数currArgs,则不用进行args的拼接
    if (currArgs !== undefined) {
        args = args.concat(currArgs);
    }

    // 递归调用
    if (args.length < fn.length) {
        return curry(fn, args);
    }

    // 递归出口
    return fn.apply(null, args);
  }
}

// 优点3:提前确认:可以在不同条件下返回不同的函数,省去重复的判断过程
// before
var on = function(element, event, handler) {
  if (document.addEventListener) {
    if (element && event && handler) {
        element.addEventListener(event, handler, false);
    }
  } else {
    if (element && event && handler) {
        element.attachEvent('on' + event, handler);
    }
  }
}
// after
var on = (function() {
  if (document.addEventListener) {
    return function(element, event, handler) {
        if (element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    };
  } else {
    return function(element, event, handler) {
        if (element && event && handler) {
            element.attachEvent('on' + event, handler);
        }
    };
  }
})();

数组的常用方法

arr.push() // 从后面添加元素,返回值为添加完后的数组的长度
arr.pop() // 从后面删除元素,只能是一个,返回值是删除的元素
arr.shift() // 从前面删除元素,只能删除一个 返回值是删除的元素
arr.unshift() // 从前面添加元素, 返回值是添加完后的数组的长度
arr.splice(i,n) // 删除从i(索引值)开始之后的n个元素。返回值是删除的元素
arr.concat() // 连接两个数组 返回值为连接后的新数组
str.split() // 将字符串转化为数组
arr.sort() // 将数组进行排序,返回值是排好的数组,默认是按照最左边的数字进行排序,不是按照数字大小排序的
arr.reverse() // 将数组反转,返回值是反转后的数组
arr.slice(start,end) // 切去索引值start到索引值end的数组,不包含end索引的值,返回值是切出来的数组
arr.forEach(callback) // 遍历数组,无return  即使有return,也不会返回任何值,并且会影响原来的数组
arr.map(callback) // 映射数组(遍历数组),有return 返回一个新数组
arr.filter(callback) // 过滤数组,返回一个满足要求的数组 
arr.reduce(callback) // 对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回

普通函数和构造函数的区别

  • 构造函数也是一个普通函数,创建方式和普通函数一样,但是构造函数习惯上首字母大写;
  • 调用方式不一样,普通函数直接调用,构造函数要用关键字new来调用;
  • 调用时,构造函数内部会创建一个新对象,就是实例,普通函数不会创建新对象;
  • 构造函数内部的this指向实例,普通函数内部的this指向调用函数的对象(如果没有对象调用,默认为window);
  • 构造函数默认的返回值是创建的对象(也就是实例),普通函数的返回值由return语句决定;
  • 构造函数的函数名与类名相同;

防抖和节流

防抖:当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。
节流:当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
参考:js防抖和节流
代码:

// 防抖
function debounce(fn, wait) {    
    var timeout = null;    
    return function() {        
        if(timeout !== null)   clearTimeout(timeout);        
        timeout = setTimeout(fn, wait);    
    }
}
// 处理函数
function handle() {    
    console.log(Math.random()); 
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));
// 节流
var throttle = function(func, delay) {            
  var prev = Date.now();            
  return function() {                
    var context = this;                
    var args = arguments;                
    var now = Date.now();                
    if (now - prev >= delay) {                    
      func.apply(context, args);                    
      prev = Date.now();                
    }            
  }        
}        
// 处理函数
function handle() {            
  console.log(Math.random());        
}        
// 滚动事件
window.addEventListener('scroll', throttle(handle, 1000));

new 操作符做了什么

// 创建了一个空对象obj
var obj  = {};
// 将这个空对象的__proto__成员指向了Base函数对象prototype成员对象
obj.__proto__ = Base.prototype;
// 将Base函数对象的this指针替换成obj,然后调用Base函数
Base.call(obj);

参考:js中的new()到底做了些什么??

websocket

websocket的特点:

  • 服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话
  • 建立在 TCP 协议之上,服务器端的实现比较容易
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器
  • 数据格式比较轻量,性能开销小,通信高效
  • 可以发送文本,也可以发送二进制数据
  • 有同源限制,客户端可以与任意服务器通信
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL(ws://example.com:80/some/path)

WebSocket 是什么原理?为什么可以实现持久连接?

export default {
  name : 'websocket',
  data() {
    return {
      websock: null,
    }
  },
  created() {
    this.initWebSocket();
  },
  destroyed() {
    this.websock.close() //离开路由之后断开websocket连接
  },
  methods: {
    initWebSocket(){ //初始化weosocket
      const wsuri = "ws://127.0.0.1:8080";
      this.websock = new WebSocket(wsuri);
      this.websock.onmessage = this.websocketonmessage;
      this.websock.onopen = this.websocketonopen;
      this.websock.onerror = this.websocketonerror;
      this.websock.onclose = this.websocketclose;
    },
    websocketonopen(){ //连接建立之后执行send方法发送数据
      let actions = {"test":"12345"};
      this.websocketsend(JSON.stringify(actions));
    },
    websocketonerror(){//连接建立失败重连
      this.initWebSocket();
    },
    websocketonmessage(e){ //数据接收
      const redata = JSON.parse(e.data);
    },
    websocketsend(Data){//数据发送
      this.websock.send(Data);
    },
    websocketclose(e){  //关闭
      console.log('断开连接',e);
    },
  },
}

异步请求xhr、ajax、axios与fetch的区别比较

xhr:现代浏览器,最开始与服务器交换数据,都是通过XMLHttpRequest对象。它可以使用JSON、XML、HTML和text文本等格式发送和接收数据。

  • 优点:
    不重新加载页面的情况下更新网页
    在页面已加载后从服务器请求/接收数据
    在后台向服务器发送数据
  • 缺点:
    使用起来也比较繁琐,需要设置很多值
    早期的IE浏览器有自己的实现,这样需要写兼容代码
  • 代码:
var request = new XMLHttpRequest();
request.open("GET", "get.php", true);
request.send();
//该属性每次变化时会触发
request.onreadystatechange = function(){
	// readyState: 响应是否成功
	// 0:请求为初始化,open还没有调用
	// 1:服务器连接已建立,open已经调用了
	// 2:请求已接收,接收到头信息了
	// 3:请求处理中,接收到响应主题了
	// 4:请求已完成,且响应已就绪,也就是响应完成了
    //若响应完成且请求成功
    if(request.readyState === 4 && request.status === 200){
        //do something, e.g. request.responseText
    }
}

jQuery ajax:jQuery对XMLHttpRequest对象的封装。

  • 优点:
    对原生XHR的封装,做了兼容处理,简化了使用。
    增加了对JSONP的支持,可以简单处理部分跨域。
  • 缺点:
    如果有多个请求,并且有依赖关系的话,容易形成回调地狱。
    本身是针对MVC的编程,不符合现在前端MVVM的浪潮。
    ajax是jQuery中的一个方法。如果只是要使用ajax却要引入整个jQuery非常的不合理。
  • 代码:
$.ajax({
  type: 'POST',
  url: url, 
  data: data,
  dataType: dataType,
  success: function () {},
  error: function () {}
})

axios:Axios是一个基于promise的HTTP库,可以用在浏览器和 node.js 中。它本质也是对原生XMLHttpRequest的封装,只不过它是Promise的实现版本,符合最新的ES规范。

  • 优点:
    从浏览器中创建XMLHttpRequests
    从 node.js 创建 http 请求
    支持 Promise API
    拦截请求和响应
    转换请求数据和响应数据
    取消请求
    自动转换 JSON 数据
    客户端支持防御 XSRF
  • 缺点:
    只持现代代浏览器
  • 代码:
axios({
    method: 'post',
    url: '/user/12345',
    data: {
      firstName: 'liu',
      lastName: 'weiqin'
    }
  })
  .then(res => console.log(res))
  .catch(err => console.log(err))

fetch:fetch是低层次的API,代替XHR,可以轻松处理各种格式,非文本化格式。可以很容易的被其他技术使用,例如Service Workers。但是想要很好的使用fetch,需要做一些封装处理。

  • 优点:
    mode: 'no-cors’就可以跨域了
  • 缺点:
    fetch只对网络请求报错,对400,500都当做成功的请求,需要封装去处理
    fetch默认不会带cookie,需要添加配置项
    fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
    fetch没有办法原生监测请求的进度,而XHR可以
  • 代码:
fetch('/users.json', {
    method: 'post', 
    mode: 'no-cors',
    data: {}
}).then(function() { /* handle response */ });

参考:异步请求xhr、ajax、axios与fetch的区别比较

对异步的理解

js是单线程的,一次只能做一件事情,遇到需要等待结果的任务,如果一直等候,就会阻塞进程,异步就是可以在某个任务等待结果的时候先执行其他任务,等结果返回后再执行这个任务

你可能感兴趣的:(面试知识点整合,面试,前端,JS)