文章目录
- var和let const的区别
- 2. typeof返回那些类型
- 3. 列举强制类型转换和隐式类型转换
- 手写深度比较
- split()区别和join()的区别
- 数组的pop(),push(),unshift(),shift()分别做什么
- ajax请求get和post的区别
- 函数call和apply的区别,手写call, apply, bind
- 事件代理(委托)是什么
- 闭包是什么,有什么特性,有什么影响
- 如何阻止事件冒泡和默认行为
- 如何减少DOM操作
- 解析jsonp的原理,为什么不是真正的ajax
- document load 和 ready 的区别
- == 和 === 的区别
- 函数声明跟函数表达式的区别
- new Object()和Object.create()的区别
- 关于this的场景
- 手写apply, call, bind
- 判断字符串以字母开头,后面是字母数字下划线,长度有限制
- 手写字符串trim方法,保证浏览器的兼容器
- 如何获取多个数字的最大值
- 如何用js实现继承
- 如何捕获js程序中的异常
- 什么是JSON
- 获取当前页面url参数
- 将url参数解析为js对象
- 手写数组flatern,考虑多层级
- 数组去重
- 手写深拷贝
- 介绍一下RAF requestAnimationFrame
千里之行始于足下, 基础知识就要一个脚印一个脚印的走才能走的远.我,前端新手,找了一点可能常出现的初级前端面试题,注意,是 初级前端面试题.
每个问题的答案都是由我,也就是前端新手的我写的,所以肯定有和大家相悖的地方,欢迎大家支出错误,大家一起成长,一起进步.
前端新手的我写的
前端新手的我写的
前端新手的我写的
var和let const的区别
var
,let
和const
我认为这是ES6出来以后,肯定需要明白的知识点.
1 var
是ES5的语法,let
,const
是ES6的语法; var
存在变量提升.
那什么是变量提升?
在JavaScript中,具体是ES5之前(ES6中是没有变量提升这个说法了,假设你只使用let
和const
),var声明的变量(函数内var声明的变量)/函数表达式/函数声明的变量在词法分析阶段会被提升到变量当前的作用域的顶部,举个🌰
console.log(a);
b();
console.log(c)
var a = 5
function b() {
console.log('function b')
}
var c = function() {
console.log('c')
}
c()
// 输出结果
// undefined
// function b
// undefined
// c
从这里代码就能够看出,虽然在变量,函数声明之前就使用了,但是程序是不会报错的,函数正常运行,变量输入undefined
.
作用域
变量提升里面,我们说了变量在词法分析阶段会被提升到变量当前的作用域的顶部,这里也有一个作用域的知识点,
作用域就是规定了变量的合法使用范围,也就是说,同级的函数作用域/块级作用域内的变量是不能互相直接访问的(全局作用域除外),但是可以访问上级作用域,依次向上,找到需要的变量为止.
🌰
var a = 1;
function test(){
function test1(){
function test2(){
console.log(a);
console.log(b);
}
function test3() {
var b = 2;
}
test2();
test3();
}
test1()
}
test()
// 输出结果
// 1
// error: Uncaught ReferenceError: b is not defined
2 let
和var
都是变量,可以修改其值,const
常量,不能修改其值.
const
声明一个值类型的常量时, 是不能被修改的, 但如果是引用类型,比如Array
,Set
,Map
,Object
等,是可以使用引用类型堆栈存储数据和地址的特性来修改一个const
声明的常量.
const声明的常量必须初始化
3 let
和const
有块级作用域,var
没有
在ES6之前,JavaScript只用两种作用域,全局作用域和函数作用域.
全局作用域: 就是函数外声明的变量时全局的.
函数作用域: 就是函数内部声明的变量只能在函数内部使用.
而块级作用域顾名思义就是只能在一个块里面使用,{}
代表一个块级作用域,块级作用域中的使用let
,const
声明的变量常量在外部是不能不为访问到的,但是有个例外,就是在块级作用域中使用var
声明的变量,可以在外部访问.
🌰
{
var x = 1;
let y = 2;
}
console.log(x)
console.log(y)
// 输出
// 1
// error: Uncaught ReferenceError: y is not defined
2. typeof返回那些类型
首先,需要只要typeof
的作用是什么.
typeof
可以检测变量的数据类型.具体如下:
- 所用的值类型
- 函数类型
- 判断是否是引用类型(具体的引用类型需要使用
instanceof
来判断)
🌰:
console.log(typeof "a");// string
console.log(typeof 1);// number
console.log(typeof true);// boolean
console.log(typeof Symbol("symbol"));// symbol
console.log(typeof a);// undefined
console.log(typeof {});// object
console.log(typeof []);// object
console.log(typeof new Set());// object
console.log(typeof new Map());// object
console.log(typeof b); // function
function b(){
}
所以typeof
返回的类型如下:
值类型包含: string, boolean, number, symbol, undefined
函数类型: function
引用类型: set, map, array, null
这里有一个undefined
和null
的知识点.
undefined和null
undefined: 是作用没有进行赋值的变量的默认值
null: 表示一个没有任何地址的对象
什么时候使用null
释放内存时,可以使用null
赋值.
3. 列举强制类型转换和隐式类型转换
强制类型转换: 在代码中明确指出需要把一个值的类型转为另一类型.
比如:paseInt, paseFloat, toString
等系统提供的强制类型转换方法.
隐式类型转换: 代码没有明确,但是在使用过程中发生了类型转换.
比如: if
, 逻辑运算, ==
, +
拼接字符串.
🌰:
let a = 1
if(a) {
console.log(a) // 这里输入了 1
}
console.log(a == "1") // true
console.log(a+"abc") // 1abc
let b = a * "2"
console.log(b); // 2
隐式类型转换在平时开发中也是常常用到的.
手写深度比较
一般来说, 在对比两个值的是否相等的时候,我们一般使用的是===
,但是这个是有局限性的,比如对象,因为对象即使值是一样的,他们的地址也是不同的.
这个题主要的知识点是递归,两个对象的深度比较,需要将他们的每一项都进行对比.
上🌰:
function deepCompare(obj1, obj2){
// 判断传入的参数不为空, 并且是object类型
if(obj1 == null || obj2 == null || typeof obj2 !== 'object' || typeof obj1 !== 'object') {
return obj1 === obj2
}
if(obj1 === obj2) return true
const obj1Keys = Object.keys(obj1)
const obj2Keys = Object.keys(obj2)
if(obj1Keys.length !== obj2Keys.length) return false
for(let key of obj1Keys) {
let result = deepCompare(obj1[key], obj2[key])
if(!result) {
return false
}
}
return true
}
const obj1 = {
a: 100,
b: 200,
c: {
d: 300
}
}
const obj2 = {
a: 100,
b: 200,
c: {
d: 400
}
}
console.log(deepCompare(obj1, obj2)); // false
split()区别和join()的区别
split: 将字符串分割成数组
join: 将数组合并为一个字符串
🌰
'a,b,c'.split(',') // ['a','b','c']
['a','b','c'].join('') // 'abc'
数组的pop(),push(),unshift(),shift()分别做什么
先上一个🌰,再来说:
let arr = [1,2,3]
console.log(arr.pop()) // 3
console.log(arr) // [ 1, 2 ]
console.log(arr.push(4)) //3
console.log(arr) // [ 1, 2, 4 ]
console.log(arr.unshift(-1,0)) // 5
console.log(arr) // [ -1, 0, 1, 2, 4 ]
console.log(arr.shift()) //-1
console.log(arr) // [ 0, 1, 2, 4 ]
接下来我们一个一个来说:
pop(): 从数组的尾部去一个元素,并返回这个元素.
push(): 向数组添加元素,从数组的尾部插入,可以传多个, 返回数组的长度.
shift(): 从数组的头部去一个元素,并返回这个元素.
unshift(): 向数组添加元素,从数组的头部插入,可以传多个, 返回数组的长度.
讲道理,这道题很简单的, 但是这道题包含了另一个知识点,那就是纯函数,
纯函数就是不改变原来的数组,这里就是arr
,并且返回的也是一个数组,可以看到,上诉代码中的四个方法,都改变原数组,也不返回一个数组,也就是说,这个几个函数都不是纯函数.还有forEach, some, every, reduce
都不是纯函数.
当然,在数组的方法中,是有纯函数的方法的.
concat
concat
可以向一个数组追加一个数组,并且在不改变原数组返回一个新的数组.
🌰
let arr = [1,2,3]
let new_arr = arr.concat({4,5,6})
console.log(arr) // [1,2,3]
console.log(new_arr) //[1,2,3,4,5,6]
map
遍历数组的每一项,并返回一个数组:
🌰
let new_arr = arr.map(item => return item * 10) // [10,20,30]
filter
遍历数组的每一项, 并返回一个经过条件判断过滤后的数组.
🌰
let new_arr = arr.filter(item => return item > 1) // [2,3]
slice
slice(切片)可以在不修改原数组情况复制数组在某个范围内的元素.
slice在不传递值的时候,类似于一个深拷贝的操作,是类似于.
🌰
let new_arr = arr.slice() // [1,2,3]
new_arr = arr.slice(1) // [2,3]
new_arr = arr.slice(0, 2) // [1,2]
new_arr = arr.slice(2) // [3]
new_arr = arr.slice(-1) // [3]
slice只传一个值的时候, 会默认第一个值为数组的长度
splice(非纯函数)
因为slice
和splice
命名有点相似,这也说一下splice
🌰
let new_arr = arr.aplice(1,2,0,0,0,0)
console.log(arr) // [3]
console.log(new_arr) // [0,1,0,0,0,0]
splice
剪接原数组,并且可以在剪接的同时在原来的位置填充元素.
或者不填充元素arr.aplice(1,2)
,或者是第二只默认为数组的长度arr.aplice(1,0,10,10,10,10)
ajax请求get和post的区别
- get一般用于查询操作,post一般用于提交数据操作
- get参数拼接在url上,post放在请求体内
- post可以防止CSRF
函数call和apply的区别,手写call, apply, bind
首先,call
和apply
都是用来修改函数中this的指向的,并且立即执行函数.🌰
function fruits(){}
fruits.prototype = {
color: "red",
say: function(){
console.log(this.color);
}
};
var another = {
color: "yellow"
};
var apple = new fruits;
apple.say(); // red
apple.say.call(another); // yellow
apple.say.apply(another); // yellow
他们的区别就在于传入的参数方式不同, call
参数是一个一个的,apply
是一个数组或者类数组.还是上面的🌰
function fruits(){}
fruits.prototype = {
color: "red",
say: function(){
console.log(this.color, arguments);
}
};
var another = {
color: "yellow"
};
var apple = new fruits;
apple.say(); // red
apple.say.call(another,1,1,1); // yellow { '0': 1, '1': 1, '2': 1 }
apple.say.apply(another, [2,2,2]); // yellow { '0': 2, '1': 2, '2': 2 }
事件代理(委托)是什么
事件代理是基于事件冒泡来做的,所以,需要知道,什么是事件冒泡.
事件冒泡
事件的操作沿着DOM结构一级一级向上传递,这种传递过程就叫事件冒泡🐶
🌰
<div>
<p id='p1'>p1</p>
<p id='p2'>p2</p>
<p id='p3'>p3</p>
</div>
const p1 = document.getElementById('p1')
const div = document.querySelector('div')
p1.addEventListener('click', e => {
alert('p1')
})
div.addEventListener('click', e => {
alert('div')
})
在浏览中,会先弹出p1,再弹出div.
当然,也可以阻止事件冒泡, 只需要添加stopPropagation
方法.
p1.addEventListener('click', e => {
e.stopPropagation()
alert('p1')
})
事件冒泡说了,就继续来说事件代理.
事件代理
场景: 瀑布流
比如有这样一个需求,一个div
里面可以无限制的添加a
标签,但是需要在点击a
标签的时候知道点击的是哪个a
标签,我们不可能一个一个的去绑定是事件,所以,就需要事件代理机制来实现这个需求.
把事件绑定到div
上面,利用事件冒泡机制,触发div
的事件,利用特殊手段得到点击的a
标签.
🌰
<div>
<a href='#'>a1</p>
<a href='#'>a2</p>
<a href='#'>a3</p>
<a href='#'>a4</p>
<a href='#'>a5</p>
.........
</div>
const div = document.querySelector('div')
div.addEventListener('click', e => {
e.preventDefault() // 阻止默认行为
const target = event.target
if(target.nodeName === 'A'){
alert(target.innerHTML)
}
})
这个就是事件代理了.
事件代理优点
- 代码简洁
- 减少浏览器内存占用
闭包是什么,有什么特性,有什么影响
闭包实际上只作用的一个特殊情况,一般有两种表现:
- 函数作为参数被传递
- 函数作为返回值被返回
🌰
function test() {
let a = 1
return function(){
console.log(a)
}
}
let t = test()
let a = 2
t() // 1
function test(fn) {
let a = 1
fn()
}
let a = 2
function b(){
console.log(a)
}
test(b()) // 2
有人可能有疑惑了,为什么第一个输入1,第二个输入2.
这个就是闭包的特性了,闭包内使用的自由变量,是在定义的时候使用的,而不是执行的时候.
影响
变量会常驻内存,得不到释放. 所以闭包不要乱用
如何阻止事件冒泡和默认行为
阻止冒泡:e.stopPropagation()
阻止默认行为:e.preventDefault()
如何减少DOM操作
原因: 因为DOM操作非常的消耗性能
缓存DOM查询
const list = document.getElementById('a')
多次DOM操作,合并到一次插入
const d1 = document.getElementById('d1')
const div = document.createDocumentFragment()
for(let i = 0; i < 10; i++){
const a = document.createElement('a')
a.innerHTML = 'a'+i
div.appendChild(a)
}
d1.appendChild(div)
解析jsonp的原理,为什么不是真正的ajax
ajax是通过XMLHttpRequest
,jsonp是通过script
标签来使用的.
首先看看jsonp的代码实现:
<script>
window.a = function(data){ console.log(data) }
</script>
<script src="./jsonp.js"></script>
jsonp.js
a({name: '1234567'})
运行打印出{name: '1234567'}
这数据,这个就是jsop的实现方式.这里也能看出来,它不是真正的ajax.
document load 和 ready 的区别
window.addEventListener('load', function(){
// 页面的全部的资源加载完才会执行,包括图片和视频
})
document.addEventListener('DOMContentLoaded', function(){
// DOM渲染完就执行, 图片,视频可能还没加载完
})
== 和 === 的区别
== 会尝试类型转换
=== 是严格相等的
函数声明跟函数表达式的区别
代码说明最有效:
// 函数声明
function a(){}
// 函数表达式
let b = function(){}
首先: 函数声明式function
在前,函数名在后,函数表达式是function
在后面,赋值给一个变量或者常量.
其次: 函数声明式会在代码执行前预加载(相当于变量提升),函数表达式是把这个声明的变量提升到顶部,而函数不会预加载,如果在函数表达式之前调用函数,是无法成功调用的.
a()
console.log(b) // 这里只能使用b这个变量,而不是b()
// 函数声明
function a(){}
// 函数表达式
let b = function(){}
new Object()和Object.create()的区别
new Object()
相当于我们平常直接使用{}
来定义一个对象,原型是Object.prototype
let a = new Object()
console.log(a.__proto__ === Object.prototype); // true
而Object.create()
是可以指定原型的,当传递null
是,就表示这个由Object.create()
创建的对象是没有原型的.
🌰
let a = Object.create(null)
console.log(a.__proto__); // undefined
let b = {c: 1}
let d = Object.create(b);
console.log(d.__proto__ === b); // true
关于this的场景
这句话要记住: this取什么值,是在函数执行的时候定义的,不是在函数定义的定义的
- 普通函数中, this指向window
- call, apply, bind指向特指的对象
- 对象方法中,this指向对象. 但是在对象方法中使用了例如
setTimeOut, setInterval
的这样全局对象的函数,this还是指向的window - class中的this指向这个class对象
- 箭头函数,箭头函数的this永远取它上级作用域的this
手写apply, call, bind
bind:
Function.prototype.bind1 = function() {
const args = Array.prototype.slice.call(arguments)
const t = args.shift()
const self = this
return function() {
self.apply(t, args)
}
}
function fn() {
console.log(this.x);
}
let nfn = fn.bind1({x: 10})
nfn()
apply, call
function fnS(context) {
let fnSymbol = Math.random() + new Date().getTime().toString()
if (context.hasOwnProperty(fnSymbol)) {
return fnS(context)
} else {
return fnSymbol
}
}
Function.prototype.call1 = function () {
let args = [...arguments]
let context = args.shift() || window
let fn = fnS(context)
context[fn] = this
context[fn](...args)
delete context[fn]
}
fn.call1({ x: 10 }, 1, 2, 3)
Function.prototype.apply1 = function () {
let args = [...arguments]
let context = args.shift() || window
let fn = fnS(context)
context[fn] = this
context[fn](...args[0])
delete context[fn]
}
fn.apply1({ x: 10 }, [2, 3, 4])
判断字符串以字母开头,后面是字母数字下划线,长度有限制
一般来说,考虑正则表达式:
let reg = '/^[a-zA-Z]\w{2,20}$/'
这个表示匹配一个字符串,第一个是字母,后面是字母数字下划线,长度为3-21.
这个题的知识点在于会不会正则表达式的使用.
手写字符串trim方法,保证浏览器的兼容器
String.replace(/^\s+/,'').replace(/\s+$/,'')
如何获取多个数字的最大值
考虑浏览的兼容性:
function max(){
const nums = Array.prototype.slice.call(arguments)
let max = 0
nums.forEach(n=>{
if(n > max) max = n
})
return max
}
max(2,3,1,6,7,2,10) // 10
不考虑兼容性:
Math.max(2,3,1,6,7,2,10) // 10
最小值:
Math.min(2,3,1,6,7,2,10) // 1
如何用js实现继承
js实现继承有两种方式,一个是ES6的class
实现继承,一种是之前的prototype
实现继承.
class
class A{
constructor(){}
say(){
console.log('A')
}
}
class B extends A {
constructor(){
super()
}
}
let b = new B()
b.say() // A
prototype
function A(){}
A.prototype.say = function(){
console.log('A');
}
function B(){}
B.prototype = new A()
let b = new B()
b.say() // A
如何捕获js程序中的异常
常用的方式:try...catch()...
try{
} catch (err) {
console.error(err)
} finally {
}
利用window.onerror
自动捕获
window.onerror = function(message, source, line, col, error){
}
什么是JSON
- json是一种数据格式,本质是一段字符串
- json格式和JS对象结构一致,对JD语言更友好.毕竟json的全称是(JavaScript Obejct Model)
- window.JSON是一个全局对象, JSON.stringify, JSON.parse可以进行json和字符串相互转换
获取当前页面url参数
传统方式: location.search
新的API: URLSearchParams
(还是新的好用,注意兼容性就行)
🌰
indexl.html?a=1&b=2
, location.search
获取的数据为?a=1&b=2
将url参数解析为js对象
传统方法:
function queryToObject(){
const search = location.search.substr(1)
const res = {}
search.split('&').forEach(param => {
const arr = param.split('=')
res[arr[0]] = arr[1]
})
return res
}
使用URLSearchParams
function queryToObject(){
const res =t}
const pList = new URLSearchParams(location.search)
pList.forEach((val, key) =>{
res [key] = val
})
return res
}
手写数组flatern,考虑多层级
这样一个多层级的数组.[[1,2], 3, [4,5, [6,7, [8, 9, [10, 11]]]]]
按照顺序将这个顺序展开到一个数组中.
function flatern(arr){
const isArray = arr.some(item => return item instanceof Array)
if(!isArray) {
return arr
}
const res = Array.prototype.concat.apply([], arr)
return flatern(res)
}
数组去重
let a = [1,1,2,3,4,5,6,7,5,3,2]
使用Set
:
let arr = [...new Set(..a)] // [1,2,3,4,5,6,7]
传统方式,遍历去重:
function deduplication(arr){
let res = []
arr.forEach(item => {
if(res.indexOf(item) == -1) {
res.push(item)
}
})
return res
}
console.log(deduplication(a));
手写深拷贝
function deepClone(obj = {}) {
if(typeof obj !== 'object' || obj == null) {
return obj
}
let result
if(obj instanceof Array) {
result = []
} else {
result = {}
}
for(let key in obj) {
// 保证key不是原型的属性
if(obj.hasOwnProperty(key)) {
// 递归调用
result[key] = deepClone(obj[key])
}
}
return result
}
let obj = {
name: "Name",
address:{
city: 'chengdu'
}
}
let obj2 = deepClone(obj)
obj2.address.city = "yibin"
console.log(obj);
介绍一下RAF requestAnimationFrame
前端很多时候会使用动画,而一个流畅的动画,更新的频率需要60帧/s,也就是16ms就要更新视图.人眼才能感觉流畅,不卡顿.
一般来说,是使用setTimeout
来手动更新的,而requestAnimationFrame
是靠浏览器自动控制的.
requestAnimationFrame
还会自动暂停不需要更新的动画,而setTimeout
不会停止