1.JS的内置类型
js的内置类型分为两大类:一类是JS的基本类型包括六种
- null
- undefined
- string
- number
- boolean
- symbol
第二类是引用类型Object
面试题
1.说下你知道的JS的内置类型
2.JS继承
JS继承有7种方式:
-
原型链继承
function father() { this.type = 1 } function person(name) { this.name = name this.color = 1 } person.prototype = new father() let child1 = new person('child') let child2 = new person('child') child1.__proto__.type = 2 console.log(child1.type) // 2 console.log(child2.type) // 2 // 特点:实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性 // 缺点:所有属性都会被继承,且子元素修改一个一起都被修改,所以构造函数继承就会好一点 -
构造函数继承
function Person(name) { this.name = name this.type = 'Function', this.getType = function() { return this.name } } function child() { Person.call(this, 'sun') this.age = 12 } console.log(p1.name, p1.type, p1.getType) // 特点:虽然可以像父类传参了 // 缺点:可以向父类传参了,但是每次都要调用一次父类构造器 -
组合继承
function Person(name) { this.name = name } Person.prototype.type = 'Function' Person.prototype.getType = function() { return this.name } function child() { Person.call(this, 'sun') this.age = 12 } child.prototype = new Person() let p1 = new child() console.log(p1.name, p1.type, p1.getType) // 特点: 既可以像父类传参,也可以真正意义上的继承方法 // 缺点:俩次调用父类构造函数耗性能 -
原型式继承
function f(obj) { function child() {} child.prototype = obj return new child() } let father = { color: "yellow" } let child1 = new f(father) let child2 = new f(father) child1.color = "red"console.log(child1.color) // red console.log(child2.color) // yellow console.log(father.color) // yellow // 特点:通过指定prototype来实现继承,会继承原型上的属性 但是无法实现复用 -
寄生继承
function f(obj) { function child() {} child.prototype = obj return new child() } let father = { color: "yellow" } // 再定义一个函数把对象构建放在这个里面,然后给这个对象添加公共的属性 function last() { let child = new f(father) child.age = 12 return child } let child1 = new last() console.log(child1.color, child1.age) // yellow 12 -
寄生组合式继承
function f(superObj) { function child() {} child.prototype = superObj.prototype return new child() } // 父亲 function father(name) { this.name = name } // 孩子 function child(name, age) { father.call(this, name) this.age = age } // 下面实现继承俩个对象的继承 function jicheng(child, father) { let zhongjian = new f(father) zhongjian.constructor = child child.prototype = father return zhongjian } -
class继承
class Point { constructor(x, y) { this.x = x; this.y = y; this.type = 'class' } hello() { console.log('hello world'); } } class ColorPoint extends Point { constructor(x, y, color) { super(x, y); this.color = color; // 正确 } } let p1 = new ColorPoint(1, 2, 'red') console.log(p1)
3.作用域
我理解的作用域是分为全局作用域和函数作用域,全局作用域和函数作用域其实比较好理解,声明在某个函数内的就是函数作用域,不在函数内声明的就是全局作用域,与之相关的作用域链可以理解为向下传递值的包含关系
4.变量提升
我理解的变量提升是一个变量在声明之前被访问会不会报错
对于全局作用域来说,具有var标记的变量标识符会被提升到全局作用域,同时为其赋值undefined,具有let标记的不会提升,只有在声明之后才会被访问,具有const标记的不会被提升,在运行中也不能改变其值
对于函数作用域来说,具有var标记的变量会被提升到函数作用域的顶部,同时为其赋值undefined,具有let标记的不会被提升,只有在声明之后才会被访问,具有const标记的不会被提升,在运行这不能改变其值
5.this指向问题
- 分辨this指向哪里
对于this指向我们可以从函数分类及其调用方式理解:
对于全局作用域来说,其this指向window
对于普通函数来说,this指向调用它的对象,无调用则指向window
箭头函数是一类特殊的函数,其this指向最后一个调用它的非箭头函数
匿名函数的this指向window
对于new的构造函数来说,this指向通过new创建的对象
- 如何改变this指向
apply、call、bind
```
var person = {
voice: '11',
say: function(voice1) {
console.log(this.voice, voice1)
}
}
var femal = {
voice: '222'
}
// call
person.say.call(femal,333) // 222 333
// apply
person.say.apply(femal,[444]); // 222 444
// bind
person.say.bind(femal); // 什么都不输出
// 从上面的例子可以看出,apply和call的作用是一样的,bind的还是不一样的
// 区别就是参数传递的方式不一样,call更简洁一些,而且call的性能更好一点
this.num = 9;
var myModule = {
num: 81,
getNum: function() {
console.log(this.num);
}
}
myModule.getNum();
```
- 手写apply call bind
参考另一篇博客:juejin.cn/post/690461…
6.立即执行函数
我理解的立即执行函数就是函数声明后会被作为表达式立即执行
7.instanceof如何理解
我理解的a instanceof b是判断一个对象a的原型链上是否存在b对应的构造函数
比如:下面的示例就是在做判断
```
function person(name, age) {
this.name = name
this.age = age
}
let p1 = new person('sun', 23)
// console.log(p1 instanceof person)
```
手写instanceof
```
function f(left, right) {
let prototype = right.prototype
left = left._proto_
while(left) {
if(left == prototype) {
return true
}else {
left = left._proto_
}
}
}
```
8.闭包
我理解的闭包是一个函数内定义了另外一个函数,这个函数会访问包裹函数中的变量,这一使用称为闭包,常见的使用闭包的有防抖和节流,注册回调,封装私有变量
9.对象的遍历
对象的遍历有多种方式
-
Object.keys
-
Object.values
-
Object.entries
-
for in
-
Object.getOwnPropertyNames
-
Object.getOwnPropertySymbols
10.定时器
定时器有两种类型,一种是只执行一次的定时器setTimeout,一种是会循环执行的定时器setInterval,定义的时候会传两个参数,第一个为回调,第二个为延迟执行的时间,其this指向是window,是浏览器的宏任务
与之相关常见的知识点是事件循环相关的,面试的时候可以多准备准备事件循环机制的理解,到时候一起说
11.JSON
JSON是一种数据格式,通常我们与后台做数据传递的时候使用的数据格式就是JSON,JSON相关的两个方法是JSON.parse和JSON.stringify,与之相关的可以谈谈深拷贝时使用JSON的利弊
12.深拷贝和浅拷贝
我了解的浅拷贝的方法有四种:
-
for...in
function deepCopy1(obj) { let o = {} for(let key in obj) { o[key] = obj[key] } return o } -
Object.keys
function deepCopy1(obj) { let o = {} for(let key in Object.keys(obj)) { o[key] = obj[key] } return o } -
Object.entries
function deepCopy3(obj) { let copy = {} for(let [key,value] of Object.entries(obj)) { copy[key] = value } return copy } -
Object.assign
function deepCopy4(obj) { return Object.assign({}, obj) }
我了解的深拷贝的方法有以下:
-
JSON
function deepCopy2(obj) { let copy = JSON.stringify(obj) return JSON.parse(copy) } -
for...in
function deepCopy(obj) { let o = {} for(let key in obj) { if(typeof obj[key] == 'object') { o[key] = deepCopy1(obj[key]) }else { o[key] = obj[key] } } return o }
但是JSON的方式看似简单但是会存在一些问题:
-
undefined、Symbol、函数会被忽略
-
Date转换不准
-
正则会被忽略
var a = {n: 1}; var b = a; a.x = a = {n: 2};
console.log(a); // {n: 2} console.log(b); // {n:1, x: {n: 2}}
个人理解:
var a = {n: 1}栈中给a开辟了一个引用地址,指向堆中的对象{n: 1}
var b = a栈中给b开辟了一个引用地址,指向堆中的对象{n: 1}
.的优先级高于=,所以先执行a.x,堆内存中的{n: 1}就会变成{n: 1, x: undefined},改变之后相应的b.x也变化了,因为指向的是同一个对象。
赋值操作是从右到左,所以先执行a = {n: 2},a的引用就被改变了,然后这个返回值又赋值给了a.x,需要注意的是这时候a.x是第一步中的{n: 1, x: undefined}那个对象,其实就是b.x,相当于b.x = {n: 2}
13.callee和caller
- callee
callee是arguments的一个属性,用来返回当前正在执行的函数
- caller
caller是返回调用当前函数的函数,如果没有调用者返回null
```
function a() {
console.log(a.caller)
console.log(arguments.callee)
}
function b() {
a()
}
a() // null [Function: a]
b() // [Function: b] [Function: a]
```
14.内存泄漏和内存溢出
- 内存泄漏
内存泄漏是指有些无法回收的内存导致该内存无法被利用
- 内存溢出
内存溢出是指内存不够用,所需大于目前已有
15.类型转换
- 转string
转换为string的方法我们可以使用+、toString和String()
布尔值转换为string后是字符串true和false
Number转换为string后是字符串类型的1和NaN
null和undefined转换为string是字符串null和undefined
需要注意的是toString不能被1调用,也不能被null和undefined调用
- 转boolean
转换为布尔值的方法我们可以用Boolean()和!!
字符串转布尔值除了''为false,其它均为true,注意‘0’是true
Number除了0和NaN为false,其它均为true
null和undefined均为false
- 转Number
转换为number我们可以用Number()
String转Number除了数字可以转为Number,其它的为NaN
Boolean转Number,true为1,false为0
null和undefined均为0
16.为什么0.1 + 0.2 !== 0.3
可以参考这篇博客
17.Object和Map的区别
Object和Map都可以用来表示键值对
①从使用上:api不同
②从键值上:键值类型map可以有很多类型1和‘1’共存,对象只有字符串或者 Symbols
```
let map = new Map()
map.set('1', 9)
map.set(1, 9)
console.log(map) // Map { '1' => 9, 1 => 9 }
let obj = Object()
obj['1'] = 9
obj[1] = 9
console.log(obj) // { '1': 9 }
```
18.Object.defineProperty
我理解的Object.defineProperty是对象定义属性的api,一共需要传入三个参数
obj:操作目标对象
prop:操作目标属性
descriptor:描述对象
第一个和第二个参数比较容易理解,关键是在第三个参数
第三个参数可以设置5个配置
- enumerable:目标属性是否可以被枚举
- condigurable: 目标属性是否可以被删除
- value:目标属性的值
- writable:目标属性是否可以被赋值修改
- set: setter函数
- get: getter函数
vue2中就使用object.defineProperty来做属性的set和get
```
const sharedPropertyDefinition = {
enumerable: true, // 是否可被枚举
configurable: true, // 对象的属性是否可以被删除
get: function(){},
set: function(){}
}
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
let template = {
data: {
name:'vue'
}
}
proxy(template, 'data', 'name')
```
19.for in和for of
在使用中我们常用for in遍历对象和数组的键值,可以用for of遍历数组的值,但是不能用for of来遍历对象的值,会提示对象是is not iterable,没有迭代器
```
// for in
let arr = [1, 4]
let obj = {
name: 'a', age: '1'
}
for(let key in arr) {
console.log(key)
}
for(let key in obj) {
console.log(key)
}
for(let key of arr) {
console.log(key)
}
for(let key of obj) {
console.log(key)
} // 报错:obj is not iterable
```
20.proxy
我理解的Proxy是一个对象属性处理的代理,可以重写对象的set和get方法,与Object.defineProperty是需要每个属性都依次绑定,但是Proxy不需要,都可直接到对应的set和get方法
-
Object.defineProperty
let obj = { name: 1 } let age = 1 function proxy(obj, prop) { Object.defineProperty(obj , prop, { enumerable: true, // 是否可被枚举 configurable: true, // 对象的属性是否可以被删除 get: function() { console.log('get') return age }, set:function(val) { console.log('set') age = val } } )} proxy(obj, 'age') obj.age = 1 // set console.log(obj.age) // get 1 -
Proxy
let p = new Proxy(obj, { get: function(target, key) { console.log('proxy get') return target[key] }, set: function(target, key, value) { console.log('proxy set') target[key] = value } }) p.child = 'proxy' // proxy set console.log(p.child) // proxy get proxy
21.普通函数和箭头函数的区别
- 普通函数有this,箭头函数只有在被非箭头函数调用时才有this
- 普通函数可以使用apply bind call,箭头函数不可以
- 普通函数可以使用arguments,箭头函数只能用rest参数
- 普通函数不能作为构造函数没有原型
22.防抖和节流
```
// 防抖
var debounce = function(func, delay){
var timer = null
return function(){
var context = this;
var args = arguments;
timer = setTimeout(function(){
func.apply(context, args);
timer = null;
},delay);
}
}
//节流
var throttle = function(func, delay){
var timer = null;
return function(){
var context = this;
var args = arguments;
if(!timer){
timer = setTimeout(function(){
func.apply(context, args);
timer = null;
},delay);
}
}
}
```
23.new操作符实现了什么
我的理解new操作符其实干了三件事,第一个定义一个新的对象,让新的对象的原型指向函数的原型,第二个是利用call apply和bind重置this指向,第三个是返回这个新对象
```
function newFun(fn) {
let obj = {}
obj.__proto__ = fn.prototype
fn.call(obj)
return obj
}
```
24.函数柯里化
我对柯里化的理解就是处理函数的一个函数,作用是将一个固定传参的函数转变为可以传入任意长度个数的参数的函数,直到传入的参数等于需要的传参个数才执行,比如我们举一个例子,一个需要传入3个参数的函数,返回这三个数之和,现在将这个函数转变为可以传入任意参数只要小于三个就不执行,newFun(1)(2)(3)和newFun(1,2)(3)这两种方式都可以
```
// 柯里化是一个函数:作用是处理需要柯里化的函数
// 通过处理fn得到一个新的函数:新的函数只接受一个参数
function curry(fn, ...args) {
let len = fn.length
return function(...args1) {
let args2 = [...args, ...args1]
if(len > args2.length) {
return curry.call(null, fn, ...args2)
}
return fn.call(null, ...args2)
}
}
function add(a, b, c) {
return a + b + c
}
let newFun = curry(add)
console.log(newFun(1)(2)(3)) // 6
console.log(newFun(1,2)(3)) // 6
```
25.浮点数精度
因为JS中数字的存储标准是 IEEE754 浮点数标准,代码中使用的无论是整数还是小数,都是64位的存储位,第一位是符号位,符号位后面的11位是指数位,最后的52位是尾数位
- 11位指数位决定了数的范围
- 52位尾数决定了数的精度
解释下为什么
一个二进制1000.1101 这个二进制数用科学计数法表示是 1.0001101 * 2^3,这里的 3 (二进制是 0011)即为指数,小数点后面的即为尾数位,一共可以容纳52位,那么最大的数就是2^53 - 1,最小的数就是-2^53 + 1
但是我感觉11位符合位远远超过了53,那不是浪费吗
26.Promise原理
我理解的promise是一个控制代码执行顺序的构造器,传入的是一个需同步执行的函数,在同步代码中写明了在哪个时机去执行reslove,哪个时机去执行reject,看一个实例如何执行resolve
```
var ninjaDelayedPromise = new Promise((resolve, reject) => {
console.log("xxx")
setTimeout(() => {
resolve(1)
}, 1000)
})
ninjaDelayedPromise.then(res => {
console.log(res)
})
```
上面的例子中,我们可以观察到resolve是在同步代码console.log("xxx")执行后,再等1s执行resolve,也就是then中的内容
看一个实例如何执行reject
```
var ninjaDelayedPromise = new Promise((resolve, reject) => {
console.log("xxx")
setTimeout(() => {
reject(1)
}, 1000)
})
ninjaDelayedPromise.then(res => {
console.log(res)
}).catch(e => {
console.log(e)
})
//上面的例子中,我们可以观察到resolve是在同步代码console.log("xxx")执行后,
// 再等1s执行reject,也就是catch中的内容
```
看一个实例如何执行finally
```
var ninjaDelayedPromise = new Promise((resolve, reject) => {
console.log("xxx")
setTimeout(() => {
reject(1)
}, 1000)
})
ninjaDelayedPromise.then(res => {
console.log(res)
}).catch(e => {
console.log(e)
}).finally(() => {
console.log(111)
})
//上面的例子中,我们可以观察到resolve是在同步代码console.log("xxx")执行后,
// 再等1s执行reject,即执行catch中的内容,最后都会执行finally中的代码
```
27.手写Promise
```
function PromiseFun(f) {
this.status = 'pending';
this.resolveCallback = function() {}
this.rejectCallback = function() {}
this.catchCallback = function() {}
this.finallyCallback = function() {}
f()
function resovle(value) {
this.status = 'fulfilled'
return this.resolveCallback(value)
}
function reject(error) {
this.status = 'rejected'
return this.rejectCallback(error)
}
}
PromiseFun.prototype.then = function (f1, f2) {
this.resolveCallback = f1
this.rejectCallback = f2
}
PromiseFun.prototype.catch = function (f) {
this.catchCallback = f
}
PromiseFun.prototype.finally = function (f) {
this.finallyCallback = f
}
PromiseFun.prototype.all = function (promises) {
let count = 0, len = promises.length
promises.forEach(e => {
p.then(() => {})
}
)}
function f(resolve, reject) {
let result = ''
if(result === 200) {
resolve(result)
}else {
reject(result)
}
}
let p = new PromiseFun(f).then((v) => {
console.log(v)
}, (e) => {
console.log(e)
})
```
28.Generator
generator可以理解为一种创造迭代器对象的数据结构,可以通过for of来访问,有几个yield就会有几个对应的对象产生,每个对象包括value和done,
```
function* generator() {
yield 1;
yield 2;
yield 3;
}
for(let v of generator()) {
console.log(v) // 1 2 3
}
let Iterator = generator("xiaolu");
let result1 = Iterator.next()
console.log(result1) // { value: 1, done: false }
let result2 = Iterator.next()
console.log(result2) // { value: 2, done: false }
let result3 = Iterator.next()
console.log(result3) // { value: 3, done: false }
```
29.手写Generator
30.事件循环机制
浏览器在执行js代码的时候会遇到同步代码和异步代码,那么要等所有异步代码都执行结束就太影响用户操作,所以他将代码任务分为宏任务和微任务,每次执行的时候会先先去执一个宏任务,然后看这个宏任务中的微任务是否执行完毕,执行完毕后会再执行下一个宏任务
宏任务:主线程js代码 键盘事件鼠标事件等等 setTimeOut html解析
微任务:promise.then dom渲染 nextTick
31.JS模块化
- ES6的import和export
es6的输入是值引用,原值发生改变之后引用也会发生改变,不支持动态导入
- commonJS
commonsjs是值拷贝,原值发生变化之后引用不会发生改变,支持动态导入
- AMD CMD
32.路由
我接触的路由相关的比较多的就是vue-router中使用的,常见的方式有两种,hash和history模式,hash其实只是url的锚点发生变化,跟路由只有一个,对于history来说是一个页面对应一个页面
33.跨域
-
vue-cli中利用webpack实现代理
-
node中间件
http-proxy-middleware
- jsonp
jsonp的原理是利用script标签不存在跨域的原理,也就是手动创建一个script标签,但是缺点是只能实现get请求
- 跨域资源共享:后台框架设置cors
cors在传递cookie必须设置withCredentials = true
这篇博客感觉讲的挺好的:segmentfault.com/a/119000001…
34.v8垃圾回收机制
在我理解的v8垃圾回收机制将内存分为新生代区域和老生代区域,新生代区域是指存活时间较短的对象,比如一个局部作用域中,只要函数执行完毕之后变量就会被回收;老生代是指存活时间较长的对象,比如全局对象或者是闭包的变量
新生代区域分为from和to两部分,from是存放新生代对象,当对象的
35.数组API
先定义一个数组
let arr = [1, 'a']
-
第一部分遍历的
for(let item of arr) { console.log(item) } console.log(arr.forEach((e, i) => { console.log(e, i) })) console.log(arr.map((e, i) => { console.log(e, i) return e + i })) -
第二部分判断数组是否符合某种条件的
console.log(arr.includes(1)) // 判断是否包含元素1 true console.log(arr.some((e, i) => { // 判断是否满足某个条件:有一个值小于0 false return e < 0 })) console.log(arr.every((e, i) => { // 判断元素是否均满足某个条件:值不等于0 true return e != 0 })) -
第三部分筛选所需元素,通常原数组都不会改变
console.log(arr.slice(0,1)) // 截取数组的[start, end) [ 1 ] console.log(arr) // [ 1, 'a' ] 原数组并不会改变 console.log(arr.find((e, i) => { // 在数组中查找第一个符合某个条件的值:第一个不等于0的值 返回1 return e != 0 })) console.log(arr.filter((e, i) => { // 在数组中查找全部符合某个条件的值:不等于0的值 返回数组[1, 'a'] return e != 0 })) -
第四部分直接操作数组,原数组通常会因此变化
console.log(arr.push('b')) // 向数组尾部添加一个元素,并返回这个数组的长度 3 console.log(arr) // [ 1, 'a', 'b' ] 原数组改变 console.log(arr.unshift('b')) // 向数组头部添加一个元素,并返回这个数组的长度 4 console.log(arr) // [ 'b', 1, 'a', 'b' ] 原数组改变 console.log(arr.pop()) // 向数组尾部删除一个元素,并返回这个元素 b console.log(arr) // [ 'b', 1, 'a' ] 原数组会改变 console.log(arr.shift()) // 向数组头部删除一个元素,并返回这个元素 b console.log(arr) // [ 1, 'a' ] 原数组会改变 console.log(arr.splice(0, 1, 2)) // 向数组替换元素,并返回第三个到最后的参数 [ 1 ] console.log(arr) // [ 2, 'a' ] 原数组会改变 console.log(arr.splice(0, 0, 3)) // 第二个参数为0就向数组增加元素,并返回第三个到最后的参数 [ ] console.log(arr) // [ 3, 2, 'a' ] 原数组会改变 -
第五部分是排序,会改变原数组
let arr1 = [1, 3, 2] console.log(arr1.sort((a, b) => { // [1, 2, 3] return b - a })) console.log(arr1) // [1, 2, 3] console.log(arr1.reverse()) // [3, 2, 1] console.log(arr1) // [3, 2, 1] -
第六部分是针对所有元素做一个处理,原数组不会改变
let arr2 = ['a', 'b', 'c'] console.log(arr2.join(',')) // 将所有元素修改为按逗号拼接的字符串 a,b,c console.log(arr2) // [ 'a', 'b', 'c' ] console.log(arr2.reduce(function(accumulator, currentValue, currentIndex, array){ return accumulator + currentValue; })) // 将所有元素做加和操作 返回值abc console.log(arr2) // [ 'a', 'b', 'c' ] -
第七部分是针对多个数组进行操作的,原数组也不会改变
let arr11 = [1, 2], arr12 = [3, 4] console.log(arr11.concat(arr12)) // [ 1, 2, 3, 4 ] console.log([...arr11, ...arr12]) // [ 1, 2, 3, 4 ]
36.常见正则
37.dom操作
-
获取单个节点类
// 获取id为app的dom let dom1 = document.getElementById('app') let dom11 = document.querySelector('#app') // 获取class为fontSizeClass的类 let dom21 = document.querySelector('.fontSizeClass') // 获取标签为div的类 let dom31 = document.querySelector('div') // 获取父元素 let node = dom1.parentNode // 获取子元素 let node = dom1.childNodes // 获取前兄弟节点 let node3 = dom1.previousSibling // 获取后兄弟节点 let node4 = dom1.nextSibling -
获取集合类的
let dom2 = document.getElementsByClassName('fontSizeClass') let dom22 = document.querySelectorAll('.fontSizeClass') let dom3 = document.getElementsByTagName('div') let dom32 = document.querySelectorAll('div') -
节点操作类
// 新增 document.createElement() // 添加 node.applendChild() // 插入 node.insertBefore(newNold, oldNode) // 移除 node.removeChild()
38.四则运算
加减乘除运算
39.设计模式
设计模式目前只对观察者模式比较熟悉,依赖某个变量的对象会因变量的变化被通知改变