JS面试
JavaScript部分
1. typeof、instanceof、Object.prototype.toStirng.call()、constructor
instanceof的作用:用来判断一个引用是否属于某构造函数。
这种判断方式也没法判断区分引用类型
instanceof主要用于判断某个实例是否属于某个类型,也可用于判断某个实例是否是其父类型或者祖先类型的实例。
instanceof主要的实现原理就是只要右边变量的prototype在左边变量的原型链上即可。因此,instanceof在查找的过程中会遍历左边变量的原型链,直到找到右边变量的prototype,如果查找失败,则会返回false
let arr=[1,2,3] arr instanceof Array //结果是true /*instanceof原理*/ function instanceof(left,right){ const rightVal=right.prototype const leftVal=left.__proto__ while(true){ if(leftVal===null){ return false } if(leftVal===rightVal){ return true } leftVal=leftVal.__proto__ //获取祖类型的__proto__ } }
Object.prototype.toString.call(obj)检测对象类型。可以区分开引用类型,说白一点,就是使用这种方法可以完全区分判断所有的数据类型
console.log(Object.prototype.toString.call("jerry"));//[object String] console.log(Object.prototype.toString.call(12));//[object Number] console.log(Object.prototype.toString.call(true));//[object Boolean] console.log(Object.prototype.toString.call(undefined));//[object Undefined] console.log(Object.prototype.toString.call(null));//[object Null] console.log(Object.prototype.toString.call({name: "jerry"}));//[object Object] console.log(Object.prototype.toString.call(function(){}));//[object Function] console.log(Object.prototype.toString.call([]));//[object Array] console.log(Object.prototype.toString.call(new Date));//[object Date] console.log(Object.prototype.toString.call(/\d/));//[object RegExp] function Person(){}; console.log(Object.prototype.toString.call(new Person));//[object Object]
实现原理:toString方法返回反映这个对象的字符串,使用toString方***
2 call、apply以及bind的区别和用法
作用:他们最主要的作用,是改变this的指向。
#### 2.1 call和apply的共同点
共同点,改变上下文环境,将一个对象的方法交给另一个对象来执行,并且是立即执行的。
A对象有一个方法,而B对象因为某种原因,也需要用到同样的方法。那么这时候我们单独为B对象扩展一个方法呢,还是借用一下A对象的方法呢。在js中,借用其他对象的方法是允许的。所以借用A对象的方法,即完成了需求,又减少了内存的占用。
2.2 call和apply的区别
主要的区别,主要体现在参数的写法上。
类数组
在js中有一些对象它拥有length属性,且拥有为非负整数的属性(key),但是它又不能调用数组的方法,这种对象被称为类数组对象
拥有length属性,其他属性(索引)为非负整数(对象中的索引会被当做字符串来处理),这里你可以当做是个非负整数串来理解
不具有数组所具有的方法
可以加上Array.prototype的一些有用属性:
”push“:"Array.prototype.push"
"splice":"Array.prototype.splice"
参数写法不同
call和apply主要的区别在参数的写法上不同
call的写法
> Function.call(obj,param1,param2,param3,....paramN)
调用call的对象,必须是个函数Function
call的第一个参数,是 一个对象。Function的调用者,将会指向这个对象。如果不穿,则默认为全局对象window
第二个参数开始,可以接受任意个参数。每个参数会映射到相应位置的Function的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到Function对应的第一个参数上,之后参数为空
function func(a,b,c){ } func.call(obj,1,2,3) //func接收到的参数实际上是1 ,2 ,3 func.call(obj,[1,2,3]) //func这次接收到的参数是[1,2,3],undefined,undefined
白话文:使用call的时候传递多个参数的时候用逗号将它们隔开,一个一个的传入
*apply的写法 *
Function.apply(obj[,argArray])
首先他的调用者是Function,第一个参数是需要借用此方法的对象
第二个参数必须是数组或类数组
function func(a,b,c){ } func.apply(obj,[1,2,3]) //和func.call(obj,1,2,3)的效果是一样的,传入的实际参数是1,2,3 func.apply(obj,{ 0:1, 1:2, 2:3, length:3 }) //这种写法,就是通过类数组进行传递,效果是一样的
call的使用场景主要在实现继承的时候
2.3 bind
bind方法与apply和call比较类似,也能改变函数体内的this指向。不同的是,bind方法的返回值是函数,并且需要稍后调用,才会执行,而apply和call是立即调用。
function add(a,b){ reutrn a+b; } function sub(a,b){ return a-b; } //需求:sub对象调用add这个方法,写法如下: add.bind(sub,12,3) //不会立即执行 add.bind(sub,12,3)() //会立即执行 //等价于 add.apply(sub,[12,3]) add.call(sub,12,3) //更加常用的搞法是: function sub(a,b){ add.call(this,12,3) return a-b }

3 Array.sort()原理
V8引擎sort函数只给出了两种排序:插入排序和快速排序,数组长度小于等于10的时候用插入排序,比10大的数组则使用快速排序
sort()方法用于对数组的元素进行排序,默认是安装每个字符对应的unicode编码的大小进行升序或者降序排序
sort()方法有两种调用方式。
arr.sort(para); //para可选参数
当不传入参数的时候,arr将按照unicode的大小进行排序
如果传入参数,这个参数必须是一个函数,这个函数用来决定排序的方式,以某种方式排序,例如升序或者逆序
比较函数传入参数(a,b)a,b在原数组中的位置a在b的后面(a在左,b在右)
比较函数返回一个正值,则交换a,b的位置
比较函数返回一个负值,则两者位置不变
返回一个0.则位置不变
比较函数返回一个大于0的数,则交换a,b位置
/*实现升序操作*/ function asc(a,b){ if(a>b){ return 1; }else if(a < b){ return -1; }else{ return 0 } } /*也等价于下面这种写法*/ function asc1(a,b){ return a-b }
/实现降序的操作/
function dsc(a,b){
if(a>b){
return -1;
}else if(a<b){
return 1
}else{
return 0
}
}
/降序操作也等价于下面的写法/
function dsc1(a,b){
return b-a
}
let arr=[2,3,4,12,12,12312312,2,1212,11,2,5,2,41,2]
arr.sort(fucntion(a,b){
})
### 4 JS的垃圾回收 JavaScript中的内存管理是自动执行的,而且是不可见的。我们创建基本类型,对象、函数.......所有这些都需要内存 当不再需要某样东西时会发生什么?JavaScript引擎如何发现并清理它? #### 内部算法 1. 基本的垃圾回收算法称为**标记-清除法**,定期执行以下“垃圾回收步骤”“ - 在函数声明一个变量的时候,就将这个变量标记为**进入环境**。而当变量离开环境时标记**离开环境**。 垃圾回收器会将所有的标记离开环境的数据给回收掉 ```javascript test(){ var a=10;//被标记。进入环境 var b=20;//被标记,进入环境 } test();//执行完毕之后。a,b被标记离开环境,被回收
引用计数法
引用计数的含义是跟踪记录每个值被引用的次数,当声明了一个变量并将一个引用类型值赋给该变量时,则这个值得引用次数就是1。如果同一个值又被赋给另一个变量,则该值得应用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值得引用次数减1。当这个值得引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存回收回来。
test(){ var a={};//a的引用次数为0 var b=a; //a 的应用次数加1,为1 var c=a; //a的引用次数再加1,为2 var b={}; //a的引用次数减1 ,为1 } /*当一个变量的引用次数为0的时候,才会被回收*/
5 addEventlistener和普通onclick区别
addEventlisterner:
function modifyText() { var t2 = document.getElementById("t2"); if (t2.firstChild.nodeValue == "three") { t2.firstChild.nodeValue = "two"; } else { t2.firstChild.nodeValue = "three"; } } // add event listener to table var el = document.getElementById("outside"); el.addEventListener("click", modifyText, false);
function initElement() { var p = document.getElementById("foo"); // NOTE: showAlert(); or showAlert(param); will NOT work here. // Must be a reference to a function name, not a function call. p.onclick = showAlert; }; function showAlert(event) { alert("onclick Event detected!"); }
6. 函数柯里化
柯里化是指这样一个函数(假设叫做createCurry),他接收函数A作为参数,运行后能够返回一个新的函数。并且这个新的函数能够处理函数A的剩余参数。
7.寻找两个不同元素最近的父节点
假设现在有两个Node......(好累,不像抄了)
8. 如何给localStorage设置一个过期时间
基础介绍localStorage的使用:
localStorage.setItem('test',1234567) let test=localStorage.getItem('test') localStorage.removeItem('test') localStorage.clear() //得到某个索引的key: localStorage.key(index);
9 页面生命周期
页面生命周期
https://www.cnblogs.com/cc-freiheit/p/12125687.html
1 DOMContented 浏览器已经加载了html ,DOM树已经构建完毕,但是img和外部样式表等资源可能还没有下载完毕
DOMContentloaded时间有document对象触发,所有的dom节点都创建好之后触发。
这里需要注意的是js的加载顺序
脚本: 浏览器的UI渲染线程和JS引擎是互斥的,当JS引擎执行时UI线程会被挂起。因此,当浏览器在解析HTML时遇到时,将不会继续构建DOM树,转而去解析、执行脚本,所以DOMContentLoaded有可能在所有脚本执行完毕之后触发。但是实际的开发中,一般不会这样做,因为出现这样的情况,白屏时间比较长,用户无法接受。常用的做法:将script标签放在body最后面,让script脚本最后加载或者采用h5的方式,加上defer属性,延迟加载js脚本,或者使用async属性
async defet 顺序 带有async的脚本是优先执行先加载完的脚本,即他们在页面中的顺序并不保证他们的顺序。遇到sync属性的script标签,会继续往下解析,并且同时另开进行进程下载脚本,当脚本下载完毕。浏览器停止解析,开始执行脚本,执行完毕后继续往下解析 带有defer的脚本时按照他们在页面中出现的顺序依次执行 DOMContented 带有async的脚本也许会在页面没有完全加载完之前就加载了,这种情况会在脚本很小或本缓存,并且页面很大的情况下发生 带有defer的脚本会在页面加载和解析完毕后执行,刚好在DOMContentLoaded之前加载(加载完就触发DOMContentLoaded)
2 load 浏览器已经完全加载了所有资源
3 beforeunload 用户即将离开页面
4 unload 用户离开页面
5 readyState document.readState这个只读属性可以告诉程序当前文档加载到哪一个步骤,它三个值:、
loading - 加载。document(dom树仍然在构建)仍在加载中;
interactive- 互动,文档已经完成加载,文档已经被解析,但是诸如图像,样式表和框架之类的子资源仍然在加载
complete: 文档和所有子资源已经完成加载。状态表示load事件即将被触发
而这个属性的每次改变同样有一个时间可以监听:
document.addEventListener('readystatechange',()=>console.log(document.readyState))
10 JS加载顺序、执行原理与性能关系
HTML页面渲染过程
开始
浏览器创建一块新的栈内存用于执行HTML代码,首先创建document对象,开始解析,创建HTMLElement对象,添加到document中,该阶段在documentloaded之前,readyState='loading'
加载css
解析顺序安装文档的顺序从上往下解析,这个阶段一直是渲染引擎在解析,当碰到head标签时,碰到外部css文件,渲染引擎会为其创建渲染引擎线程加载css。主线程继续解析文档流
加载js脚本
当碰到script标签时,技术的发展,有两种加载形式。同步加载JS脚本,这也是默认的原始方式。因为js引擎和渲染引擎是互斥的,所有当加载js脚本时候,渲染引擎会被阻塞掉,不会再解析文档。当脚本解析完毕之后,才会继续run 渲染引擎,解析文档流。但是这样一般会出现bug,就是脚本中如果有队dom节点的操作的话,会报错,因为文档还没有解析完,dom树没有构建完。所以一般做法是将script脚本放在body的最后面执行,这个时候的dom节点已经构建完成。
异步加载脚本
在现代技术尤其是在h5中,加入了defer和async属性,加入了延迟加载和异步加载的方式来加载脚本
其实defer的延迟加载和将脚本放在body部分的最后面是一样的。
加载图像
解析文档时,遇到img等,会单独创建渲染引擎线程,加载图像,渲染引擎的主线程继续解析文档
当文档解析完成,即文档流将body部分都解析完成,所有的dom节点都已经创建完成。此时DOMContented已经被触发,document.readyState='interactive' .但是这个阶段想图像,音频,视频等资源可能都还没加载完成
DOMContented时间的触发标志着程序执行从同步脚本执行阶段,转化为事件驱动阶段
当线程加载完CSS文件后。生成CSSOM,结合DOM生成render tree,然后进行回流与重绘最后显示出来,这个时候页面就显示出来了
当所有img、音频加载完成后,document.readyState='complete', window对象触发load事件
11 为什么setTimeout会出现误差
原因:marcotask中的执行时间大于setTimout中执行时间。如果当前执行栈所花费的时间大于定时器时间,那么定时器的回调会在宏任务里,来不及去调用,所以会产生时间误差
重写set(存入)方法:
/* @param {[type]} key [键名] @param{[type]} value[键值] @param{[type]} days [保存的时间(天)] */ set:function(key,value,days){ if(!value){ localStorage.removeItem(key) }else{ var Days=days||7 //默认保留7天 var exp=new Date(); localStorage[key]=JSON.stringify({ value, expires:exp.getTime()+Days*24*60*60*1000 }) } } get:function(name){ try{ let o=JSON.parse(localStoragep[name]) if(!o||o.expires<Date.now()){ return null }else{ return o.value } }catch(e){ return localStorage[name] }finally{ //随便写点啥 } }
12 this的指向问题
全局环境中的this
- 浏览器环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this都指向全局对象window
- node环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部),this都是空对象{}
其他环境下都是,看你在看什么时候调用,就是指向谁,还要注意call apply可以改变上下文环境,就是改变this的指向
特别说明下:箭头函数的情况,箭头函数没有自己的this,继承上下文绑定的this
let obj={ age:20, info:function(){ return ()=>{ console.log(this.age); //this继承的外层上下文绑定的this } } } let person={age:28} let info=obj.info() info();//20 let info2=obj.info.call(person) info() //28
13 数据类型和javaScript内存
1 js数据类型
常用的六种基本(简单)数据类型
- Boolean
- String
- Number
- Undefined
- NULL
- Symbol
复杂数据类型
Object
Object不是一个对象,这时JS历史遗留问题
2 复杂数据类型和基本数据类型的区别
内存分配不同: 基本数据类型主要存在栈中,复杂数据类型主要存在堆中
复杂数据类型的还在栈中村放一个地址值,这个地址指向堆中存储的复杂数据类型数据的真实值
访问机制不同:基本数据类型主要是值引用(访问),复杂数据类型主要是地址引用(访问)
赋值是不同(a=b)
- 基本数据类型:a=b; 将b的值的副本赋给了变量a,两者是完全独立的
- 复杂数据类型: a=b;将b的地址值给了变量a,最终a和b都指向同一个地址。两者不是完全独立的,如果真实值发生改变,a和b的值也会相应的发生改变
参数传递的不同(实参/形参)
函数传参:基本数据类型,拷贝的是值;复杂数据类型,拷贝的是引用地址
14 如何让(a==1&&a==2&&a==3)的值为true
1 for of和for in
for of适用于可迭代的对象,数组,map , set, String , TypedArray 但是不适用于一个Object对象 {}
for in 主要用来*遍历Object{} *于Object.keys的用法是一样的
let onj={a:1,b:2,c:'ss',w:'sbdf',wome:'s'} for(let item of onj){ console.log(onj[item]) }
2 理解使用ES6中的Symbol
这是一种新的基础数据类型(primitive type),Symbol是由ES6规范引入的一项新特性,它的功能类似于一种标识唯一性的ID.
let s1=Symbol() let s2=Symbol('another symbol') /* 需要主要的地方 1. Symbol不能new 出来 2.Symbol的每个实例都是唯一的独一无二的,不存在某个实例A和实例B相等的情况 */ let s3=new Symbol('I AM BIRD') //错误 let s4=Symbol('s') let s5=Symbol('s') s4===s5 //结果为false
应用场景1:使用Symbol来作为对象属性姓名(key)
使用Object.keys和for ... in来遍历对象属性时,不会遍历Symbol属性的值
let obj = { [Symbol('name')]: '一斤代码', age: 18, title: 'Engineer' } Object.keys(obj) // ['age', 'title'] for (let p in obj) { console.log(p) // 分别会输出:'age' 和 'title' } Object.getOwnPropertyNames(obj) // ['age', 'title']
也正因为这样一个特性,当使用JSON.stringify()将对象转换为JSON字符串的时候,Symbol属性也会被排除在输出内容之外:
JSON.stringify(obj) // {"age":18,"title":"Engineer"}
我们可以利用这一点,来设计我们的数据对象,让对内操作和对外选择输出变得更加优雅
如果要获取Symbol方式定义的对象属性,可以是使用Symbol的api
// 使用Object的API Object.getOwnPropertySymbols(obj) // [Symbol(name)] // 使用新增的反射API Reflect.ownKeys(obj) // [Symbol(name), 'age', 'title']
应用场景2:使用Symbol来替代常量
const TYPE_AUDIO = Symbol() const TYPE_VIDEO = Symbol() const TYPE_IMAGE = Symbol()
引用场景3:使用Symbol定义类的私有属性/方法
注册和获取全局Symbol
let gs1 = Symbol.for('global_symbol_1') //注册一个全局Symbol let gs2 = Symbol.for('global_symbol_1') //获取全局Symbol gs1 === gs2 // true
3 (a==1)&&(a==2)&&(a==3)的值为true
方法一: 利用数据劫持 使用defineproperty
let i=1; Object.defineProperty(window,'a',{ get:function(){ return i++; } }) console.log(a==1&&a==2&&a==3)
方法二:使用es6的内置对象 proxy
Proxy对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)
语法:const p=new Proxy(target, handler)
target:要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组、函数,甚至另一个代理)
handler:一个通常以函数为属性的对象
let a=new Proxy({},{ i:1, get:function(){ return ()=>this.i++; } }) console.log(a==1&&a==2&&a==3);//true
方法三 :数组的toString接口默认调用数据的join方法,重写数组的join方法
let a=[1,2,3] a.join=a.shift; console.log(a==1&&a==2&&a==3);//true
15 防抖(debounce)与节流(throttle)函数
防抖:指触发事件后在n秒内函数只执行一次,如果在n秒内又触发了事件,则会重新计算函数执行时间、
节流: 指连续触发事件但是在n秒钟只执行一次函数。节流会稀释函数的执行效率
举例说明: 小思最近在减肥,但是她非常贪吃。为此,与其男朋友约定好,如果10天不吃零食,就可以购买一个包(不要问为什么是包,因为包治百病)。但是如果中间吃了一次零食,那么就要重新计算时间,直到小思坚持10天没有吃零食,才能购买一个包。所以,管不住嘴的小思,没有机会买包(悲伤的故事)...这就是防抖。
不管吃没吃零食,每10天买一个包,中间想买包,忍着,等到第十天的时候再买,这种情况是节流。如何控制女朋友的消费,各位攻城狮们,get到了吗?防抖可比节流有效多了!
作者:刘小夕
链接:https://juejin.im/post/5cea6e5fe51d45775e33f4de
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
防抖应用场景:
1 搜索框输入查询,如果用户一直在输入中,没有必要不停的调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
表单验证
按钮提交事件
浏览器窗口缩放,resize事件等。
防抖分为非立即执行版和立即执行版
非立即执行版:是触发事件后函数不会立即执行,而是在n秒后执行,如果在n秒内触发了事件,则会重新计算函数执行时间
立即执行版:是触发事件后函数立即执行,然后n秒内不触发事件才能继续执行函数的效果
function debounce(func,wait, immediate=true){ let timeout; return function(){ let context=this; let args=arguments; if(timeout){ clearTimeout(timeout) } if(timeout){ var callNow=!timeoutl; 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) } } } }
16 Reflect对象
就是一个工具类对象,提供好多静态方法
reflect是一个内置的对象,它提供拦截JavaScript操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。
reflect对象与proxy对象一样,也是es6为了操作对象而提供的新API.Reflect对象的设计目的有这样就几个
- 将Object对象的一些明显属于语言内部的方法(比如Objeect.defineProperty),放到reflect对象上
- 修改某些Object方法的返回结果,让其变得更合理,比如,Object.defineProperty(obj,name,desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj,name,desc)则会返回false
- 让Object操作都变成函数行为。某些Object操作是命令式,比如 name in obj和delete obj[name],而Reflect.has(obj,name)和Reflect.deleteProperty(obj,name)让它们变成了函数行为。
*Reflect对象一共有13个静态方法 *
1:Reflect.get(target,name.receiver)
target:目标对象
name:使我们要读取的属性
recevier(可选):可以理解为上下文this对象
const obj={ name:'hello', age:18, get:function(){ console.log(this.name) console.log(this.age) } } console.log(Reflect.get(obj,'age'));
reflect.set(target,name,value ,recevier)
设置该对象的属性值
const obj={ name:'hello', age:18, gender:1, set:function(gender){ this.gender=gender }, get:function(){ console.log(this.name) console.log(this.age) } } const res=Reflect.set(obj,'data',31) console.log(res)
17 Proxy
https://www.cnblogs.com/dengxiaoning/p/11681242.html
什么是'代理',代理:就是调用new创建一个和目标(target)对象一直的虚拟化对象,然该代理中就可以拦截JavaScript引擎内部目标的底层对象的操作;这些底层操作被拦截后回厨房相应的特定操作的陷阱函数let tartget = {}; let proxy = new Proxy(target,{}); proxy.name = 'proxy'; console.log(proxy.name); // proxy console.log(tartget .name); // proxy tartget .name = 'tartget'; console.log(proxy.name); // target console.log(tartget .name); // target
如proxy.name='proxy';将proxy赋值给proxy.name时,代理就会将该操作转发给目标,执行name属性的创建;然而他只是做转发而不会存储该属性;so他们之间存在一个相互引用;target.name设置一个新值后,proxy.name值也改变了
function parent(name){ this.name=name } /*构造继承*/ function son(sonName){ parent.call(this,sonName) /*自己的代码*/ } /*使用原型链继承*/ function son1(){ /*自己的代码*/ } son1.prototype=new Parent() /*还有组合继承和寄生继承*/