一、js基础
1.数据类型检测
2. 内存管理和作用域
2.1 数据结构
js常见的三种数据结构:栈(stack) 、堆(heap)、 队列(queue)
- 栈(stack)
栈的特点是"LIFO,即后进先出(Last in, first out)"。数据存储时只能从顶部逐个存入,取出时也需从顶部逐个取出。 - 堆(heap)
堆的特点是"无序"的key-value"键值对"存储方式。例如图书馆的书籍,按索引摆放。 - 队列 (queue)
队列的特点是是"FIFO,即先进先出(First in, first out)" 。数据存储是从队尾插入,从队头取出。与栈的区别是:有入口和出口,而栈都是从顶部出入。
2.2 栈、堆、队列在JavaScript中的应用
- 函数执行
JavaScript中函数的执行过程,其实就是一个入栈出栈的过程:当脚本要调用一个函数时,JS解析器把该函数推入栈中(push)并执;当函数运行结束后,JS解析器将它从堆栈中推出(pop)。 - 内存存储(栈、堆)
JavaScript中变量类型有两种:基础类型(string number boolean null undefined symbol)、引用类型(Objec array function)
js的基础类型的有固定的大小,保存在栈内存中,由系统自动分配存储空间,因此基础类型都是按值访问,也遵循栈后进先出的原则。
js的引用类型,它们的值大小是不固定的,是保存在堆内存中的对象。js不允许直接访问堆内存中的位置,因此在操作的是对象的引用而不是实际对象,这里的引用可以理解为存在栈中的地址,这个地址与堆内存中真正的值有关联。
2.3 相关面试题
-
谈谈垃圾回收机制?
js具有自动回收垃圾机制,也就是说执行环境会管理代码执行过程中使用的内存。原理:垃圾收集器会定期找出那些不再使用的变量,然后释放其内存。主要有两种实现方式:引用计数和标记清除。
引用计数会追踪值被引用的次数,当引用类型的数据被赋给其他变量时,引用次数会加1。当引用计数为0时,就可以将其占用的内存空间回收。 循环引用时会导致垃圾无法回收,浪费大量内存系统,例如obj1.a=ojb1;(你中有我,我中有你,会造成obj1.a的无限循环) 标记清除是指当变量进入环境时,这个变量标记为“进入环境”,而当变量离开环境时,则将其标记为“离开环境”,最后垃圾收集器销毁带标记的值和回收它们占用的内存空间。 -
浅拷贝和深拷贝有什么区别,如何实现?
浅拷贝只复制对象的第一层属性,当对象的属性是引用类型时,实质复制的是其引用。深拷贝会创造一个一模一样的对象,会申请一块新的内存空间,修改新对象不会改到原对象。
浅拷贝实现方式:for循环,assign、es6扩展运算符(...); 深拷贝实现方式:obj2 = JSON.parse(JSON.stringify(obj1);递归赋值。
具体代码实现:// 实现浅拷贝 <!--1--> let shallowCopy = obj => { let rst = {}; // 遍历对象 for (let key in obj) { // 只复制本身拥有的属性(非继承过来的属性)枚举属性 if (obj.hasOwnProperty(key)) { rst[key] = obj[key]; } } return rst; } <!--2--> let obj1 = Object.assign({}, obj2); <!--3--> let obj1 = {...obj2}<!--深度拷贝--> <!--1--> let obj1 = JSON.parse(JSON.stringify(obj2)); <!--2--> let deepClone = obj => { let newObj = Array.isArray(obj) ? [] : {}; if (obj && typeof obj === 'object') { for (let key in obj) { if (obj.hasOwnProperty(key)) { if (obj[key] && typeof obj[key] === 'object') { newObj[key] = deepClone(obj[key]); } else { // 如果不是对象直接拷贝 newObj[key] = obj[key]; } } } } return newObj; }
2.4 执行环境和作用域
- 作用域
分为静态作用域(词法)和动态作用域
静态作用域指的是一段代码,在它执行之前就已经确定了它的作用域;动态作用域–函数的作用域是在函数调用的时候才决定的;// 静态作用域: var a = 10; function fn() { var b = 1; console.log(a + b); } fn(); // 11
fn在创建后就确定了它可以作用哪些变量,从局部找到全局。
js // 动态作用域: function foo() { console.log(a); } function bar() { var a = 3; foo(); } var a = 2; bar(); // 2;
foo在bar中调用,就会从foo找到bar。
- 执行栈、执行环境、作用域链
执行栈被用于在代码执行阶段存储所有创建过的执行环境。当Javascript引擎首次运行到你的脚本时,它会创建一个全局执行环境,并把它推入到当前的执行栈中。每当引擎运行到其函数调用时,就会为这个函数创建一个新的执行环境,并把它推入到堆栈的顶部。
在调用(执行)函数时,会为函数创建一个执行环境,然后通过复制函数的 [[scopes]] 属性中的对象构建起执行环境的作用链域。 执行环境定义了变量和函数有权访问的其他数据,而每个执行环境都有一个与之相关的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
而作用域链保证了执行环境对所以有权访问的变量和函数的有序访问。作用域链的前端是活动对象,如果环境是函数,那么它的变量对象就是该函数的活动对象,活动对象最开始只包含一个变量即arguments对象。 作用域链的下一个变量对象来自包含(外部)环境,再下一个变量对象来自下一个包含环境。
二、函数
1. 函数中的this
- 对象方法调用
当函数作为某个对象的方法调用时,this等于那个对象
//没有明确的调用对象的时候,将对函数的this使用默认绑定:绑定到全局的window对象。
var obj = {
a: 1,
foo: function () {
console.log(this.a)
function bar(){
console.log(this)//window
}
bar()
}
}
obj.foo(); // 输出1
- 构造函数调用 当以构造函数的形式调用时,this就是新创建的那个对象。
- 使用call和apply调用时
2.函数应用
- 立即执行函数
立即调用一个匿名函数。立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
//函数声明式
(function(){
})()
//函数表达式
var fun = function(){};
fun()
- 闭包
闭包指有权访问另一个函数作用域中变量的函数。创建闭包常见的方式,就是在一个函数的内部创建另一个函数。
一般情况下,函数执行完毕后,局部活动对象会被销毁,内存中仅保存全局作用域。而下面这个例子,Person执行环境在函数执行完毕后销毁,Person活动对象没有被销毁,因为Person活动对象仍然在被getName和setName这两个闭包引用。
function Person(name) {
this.getName = function() {
return name;
};
this.setName = function(value) {
name = value;
};
}
var person = new Person("Candy");
alert(person.getName()); //"Candy"
person.setName("Greg");
alert(person.getName()); //"Greg"
//常见的定时器例子
function test2(){
var j = 1;
for(var i = 0; i < 5;i ++){
(function(j){
setTimeout(function(){
console.log(j)
},1000) // 5个setTimeout任务都是等待3s执行,所以3秒一过,大家都输出。
})(i)
}
//立即执行函数形成块级作用域,将i依次赋值给j,主线程执行完毕后,闭包中的定时器开始执行。
}
//计数器例子
var getCounter = function(){
var count = 0;
return function(){
++count;
console.log(this)
return count;
}
}
var counter = getCounter();
//注意每次调用getCounte都会产生一个新的执行环境,此时只调用一次getCounter,执行完毕后,由于内部函数还在引用活动活对象,所以count没有被销毁。
counter()//1
counter()//2
2. 异步编程
2.1 理解js单线程异步
- 异步同步
同步是指需要等待事件完成后,再返回。而异步是指调用事件后,马上返回。调用者不必等待结果,调用事件得到结果后会主动通知调用者。 阻塞与非阻塞是指等待状态。阻塞(Blocking)是指调用在等待的过程中线程被“挂起”(CPU资源被分配到其他地方去了)。非阻塞(Non-blocking)是指等待的过程CPU资源还在该线程中,线程还能做其他的事情。 - 浏览器线程
首先了解一下进程和线程:浏览器的进程如下:
- GUI渲染线程
负责渲染页面,布局和绘制,页面需要重绘和回流时,该线程就会执行。 - JS引擎线程
负责处理解析和执行javascript脚本程序,只有一个JS引擎线程(单线程),与GUI渲染线程互斥,防止渲染结果不可预期。 - 定时触发器线程
setInterval与setTimeout所在的线程,定时任务并不是由JS引擎计时的,是由定时触发线程来计时的,计时完毕后,通知事件触发线程。 - 事件触发线程
用来控制事件循环(鼠标点击、setTimeout、ajax等),当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中。 - 异步http请求线程
浏览器有一个单独的线程用于处理AJAX请求,当请求完成时,若有回调函数,通知事件触发线程。 js是单线程语言是指JavaScript引擎线程只有一个,但是浏览器是多线程的,JavaScript引擎线程是浏览器分配给js的主线程,用来执行任务(函数);其他可以称为辅助线程,这些辅助线程便是JavaScript实现异步的关键。
- GUI渲染线程
2.2 定时器
- 定时器流程图
- 定时器应用场景:防抖节流
- 防抖是指触发后,延迟设定时间后执行,如果在中途触发,定时器会被清除,重新开启定时器。也就是说,只有触发间隔大于定时器delay时间,才能执行真正的函数。
function debounce(fn,delay){ var timer=null; return function(){ clearTimeout(timer) timer = setTimeout(()=>{ fn.call(this) },delay) } };- 节流是指触发后,真正的函数会在设定的延迟时间后被执行。如果在中途触发,也不会重新计时或者清除。如果真正的函数执行完毕后,会将flag设置为true,重新开启定时器。
function throttle(fn,delay){ var flag = true; return function(){ if(!flag){ return; } flag = false; setTimeout(()=>{ fn.call(this); flag = true; },delay) } }
2.3 eventLoop机制
- 宏任务和微任务
- eventLoop解析
JS 分为同步任务和异步任务,同步任务都在JS引擎线程上执行,形成一个执行栈,事件触发线程管理一个任务队列,异步任务触发条件达成,将回调事件放到任务队列中 执行栈中所有同步任务执行完毕,此时JS引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行。 eventLoop主要流程:
1.全局Script代码执行完毕后,执行栈会被清空。
2.从微队列microtask queue的队首取出回调任务,放入执行栈中执行,执行完后microtask queue长度减1,再从队首取出放入执行栈,直到所有任务执行完毕。如果在执行microtask的过程中,又产生了microtask,会加入到队列的末尾,也会在这个周期被调用执行。
3.microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空。
4.宏队列macrotask一次只从队列中取一个任务执行,执行完毕后,调用栈Stack为空;查询是否有微任务,有则执行完后就去执行微任务队列中的任务;
5.重复2-4步骤...
2.4 异步编程方法
主要有回调函数、事件发布/订阅、promise、generator函数、async函数。
- 事件发布/订阅
三、面向对象
1. 相关方法
<!--判断person1的[[prototype]]是否指向Person.prototype-->
<!--isPrototypeOf判断一个对象是否存在另一个对象的原型链上-->
Person.prototype.isPrototypeOf(person1)
<!--获取一个对象的[[prototype]]-->
Object.getPrototypeOf(person1)
将A的[[prototype]]指向B
Object.setPrototypeOf(A,B)
<!--判断一个属性是否存在于实例中,注意不是继承的属性-->
person1.hasownProperty('name')
<!--获取实例对象的所有属性,无论是否枚举-->
Object.getOwnPropertyNames(Person.property)
2. 原型和原型链
只要创建一个新函数,这个函数就会有一个prototype属性,而这个prototype属性指向函数的原型对象。而原型对象会自动获得constructor属性,指向构造函数(constructor等于该构造函数)。当调用构造函数创建一个实例后,实例的内部会有一个[[prototype]]属性,将指向原型对象。
实例会先查找自身是否有属性,没有再沿着原型链一层一层查找。实例对象不能修改原型上的属性,但是可以直接赋值该属性,从而覆盖原型上的属性。
原型对象是通过new Object生成的,属于Object构造函数的实例,它的[[prototype]]会指向Object构造函数的prototype。
3. 继承
重写子类的原型,将父类的实例赋值给子类的原型对象
参考网址:
1.www.cnblogs.com/slly/p/1036…
2.segmentfault.com/a/119000001…
3.www.fly63.com/article/det…