错误之处,欢迎指正,持续更新中。
1. 基础
1. ==
和===
的区别是什么?
console.log(1 == '1'); //true
console.log(1 === '1'); //false
使用===
进行比较时,如果等号两边的数据类型不同,直接返回false
;
使用==
进行比较,会先进行类型转换,再进行比较。
2. 运行下列代码在控制台输出的结果是?
for(var index = 0; index < 5; index ++){
var result = index + '';
}
console.log(result);
console.log(index);
console.log((1 - 'result') == (1 - 'result'));
输出结果为'4'
、5
和false
。
3. "2" + 3 + 4
的结果为?
结果为"234"
。
4. 以下哪些数据经过fn
处理后,返回的是false
?
function fn(a) {
return !!a;
}
A. NaN
B. -1
C. ""
D. "0"
答案为A、C选项。
5. 下列代码的输出结果为?
var a = new Boolean(false);
console.log(typeof a)
输出结果为object
。
6. 怎样找出x
和y
哪个数值最大?
A. Math.ceil(x, y);
B. top(x, y);
C. ceil(x, y);
D. Math.max(x, y);
E. max(x,y);
正确选项为D。
7. 下列结果为true
的表达式是?
A. null instanceof Object
B. NaN == NaN
C. null == undefined
D. false == undefined
正确选项为C。
8. 将字符串转换为JSON
对象的方法是?
A. var obj = JSON.parse(str);
B. var obj = eval('(' + str + ')');
C. var obj = str.parseJSON();
D. 都是。
正确选项为A和B。
9. 如何翻转字符串?
const str = '123456';
const arr = str.split(''); //["1", "2", "3", "4", "5", "6"]
const newArr = arr.reverse(); //["6", "5", "4", "3", "2", "1"]
const newStr = newArr.join('');
console.log(newStr); //654321
10. 如何分割字符串?
-
slice
方法
按照索引值来截取字符串,左开右闭区间。
var str = '123-1234-124';
console.log(str.slice(4, 8)); //1234
-
split
方法
var str = '123-1234-124';
console.log(str.split('-')); //["123", "1234", "124"]
按照指定的字符分割字符串。
11. 如何删除字符串中的空格?
-
split
方法和join
方法
const str = '1 2 345 6789 ';
const arr = str.split(' '); //["1", "2", "345", "6789", ""]
const newStr = arr.join('');
console.log(newStr); //123456789
- 正则表达式和
replace
方法
const str = '1 2 345 6789 ';
const reg = /\s/g; //匹配到所有空白符
const newStr = str.replace(reg,''); //将空白符替换为空字符串
console.log(newStr); //123456789
-
trim
方法
只能去掉字符串首尾两端的空格。
const str = ' 1 2 345 6789 ';
const newStr = str.trim();
console.log(newStr); //1 2 345 6789
12. 如何返回下列数组的第一个值?
var myArr = [1, 2, 3, 4, 5];
A. myArr[1];
B. myArr.pop();
C. myArr[0];
D. myArr.unshift();
E. myArr.shift();
正确选项为C、E。
13. 如何区别数组和对象?
- 查询构造函数
const arr = [1,2,3,4,5];
const obj = {
name: 'chris'
}
console.log(arr.__proto__.constructor); //Array()
console.log(obj.__proto__.constructor); //Object()
instanceof
const arr = [1,2,3,4,5];
const obj = {
name: 'chris'
}
console.log(obj instanceof Array); //false
console.log(obj instanceof Object); //true
isPrototypeOf
const arr = [1,2,3,4,5];
const obj = {
name: 'chris'
}
console.log(Array.prototype.isPrototypeOf(arr)); //true
console.log(Array.prototype.isPrototypeOf(obj)); //false
-
isArray
静态方法
const arr = [1,2,3,4,5];
const obj = {
name: 'chris'
}
console.log(Array.isArray(arr)); //true
console.log(Array.isArray(obj)); //false
14. 如何翻转数组?
var arr = [1,2,3,4,5];
console.log(arr.reverse()); //[5, 4, 3, 2, 1]
console.log(arr); //[5, 4, 3, 2, 1]
要注意的是,该方法会改变原数组。
15. 简要说明splice
的用法。
splice
方法可以用来删除数组中的某些值,或者添加一些值,返回值为被删除的项目,该方法会改变原数组。
参数分别为:
- 删除的起始位置(负数是指从数组结尾开始)
- 要删除的数量
- 添加的新内容
var arr = [0, 1, 2, 3, 4, 5];
arr.splice(0, 2, 1); //[0, 1]
console.log(arr); //[1, 2, 3, 4, 5]
16. 如何实现数组去重?
-
indexOf
和includes
var arr = [1,2,3,4,5,1,5];
var newArr = [];
for (let index = 0; index < arr.length; index++) {
if(newArr.indexOf(arr[index]) == -1){
newArr.push(arr[index]);
}
}
console.log(newArr); //[1, 2, 3, 4, 5]
无论是使用indexOf
还是includes
,原理都是遍历旧数组,然后查找新数组是否已存在该项,不存在时进行push
。
SET
var arr = [1,2,3,4,5,1,5];
var newArr = Array.from(new Set(arr));
console.log(newArr); //[1, 2, 3, 4, 5]
17. 如何实现对数组和对象的深浅拷贝?
- 浅拷贝
var arr = [1,2,3,4,5];
var newArr = arr;
newArr[0] = 0;
console.log(newArr); //[0, 2, 3, 4, 5]
console.log(arr); //[0, 2, 3, 4, 5]
//arr和newArr共用同一块内存。
- 深拷贝
var arr = [1,2,3,4,5];
var newArr = [];
for (let index = 0; index < arr.length; index++) {
newArr.push(arr[index]);
}
newArr[0] = 0;
console.log(newArr); //[0, 2, 3, 4, 5]
console.log(arr); //[1, 2, 3, 4, 5]
18. 如何实现一个递归阶乘?
function getResult(num) {
if(num === 0) {
return 0;
}
if(num === 1){
return 1;
}else{
return num * getResult(num - 1);
}
}
console.log(getResult(5)); //120
19. touch
和click
以及mousedown
有什么区别?
- 触发点不同
touch
(不包括touchstart
)事件不一定发生在所监听的DOM
元素上,mouse
事件必须发生在所监听的DOM
元素上。
-
touch
touchstart
触发后,即使鼠标移出监听的DOM
区域,touchend
、touchmove
和touchend
依然可以触发。 -
mouse
、click
mousedown
触发后,鼠标移出监听的DOM
区域,mouseup
不会触发。click
也是如此,按下之后,离开目标区域,抬起鼠标,click
事件不会触发。
- 触发的先后顺序
boxDom.addEventListener('mousedown', () => {
console.log('mousedown发生了');
})
boxDom.addEventListener('mouseup', () => {
console.log('mouseup发生了');
})
boxDom.addEventListener('click', () => {
console.log('click发生了');
})
boxDom.addEventListener('touchstart', () => {
console.log('touchstart发生了');
})
boxDom.addEventListener('touchend', () => {
console.log('touchend发生了');
})
综上,
touch
事件的发生时间是早于mouse
事件和click
事件的。
20. 事件有哪几个阶段?
- 捕获阶段
事件从最不精确的对象到最精确的对象。
当我们在DOM
树上某个节点设置并发生了一些操作后,就会有一个事件触发,这个事件从Window
发出,到目标节点结束。 - 目标阶段
事件经过捕获阶段,直至目标节点,捕获阶段结束,在目标节点触发事件,此时就是目标阶段。 - 冒泡阶段
事件从最精确的对象到最不精确的对象。
事件经过目标阶段,由目标节点开始,逐级向上传递。
DOM的树形结构决定了子元素肯定在父元素之中,所以点击子元素就同时点击了子元素和父元素,以及父元素的父元素,以此类推,那么子元素的父级上的绑定事件,都会由于子元素的点击而执行。
可以通过调用stopPropagation
方法来阻止事件冒泡。
21. 为什么要使用事件委托?
事件委托是利用了事件冒泡的原理。例如有100
个li
标签,每一个都需要点击事件,那么就把这个点击事件委托给他的父元素ul
。
事件委托可以减少重复代码量,减少了事件注册,从而降低了DOM
操作,提升性能。其次就是对于新添加的子元素,无需再次绑定事件。
22. 分别说明querySelector
和querySelectorAll
的作用。
-
querySelector
document.querySelector("p");
选中文档中的第一个p
标签元素。
document.querySelector("h2, h3");
此时要看文档中的第一个h2
标签和第一个h3
标签哪一个在前面,谁在前面就选中谁。 -
querySelectorAll
document.querySelectorAll(".example");
选中文档中所有类名为example
的元素。
23. 简要描述闭包是什么,有什么特性,如何创建一个闭包?
首先,javascript
中允许在函数中声明函数:
function main() {
function help() {
}
}
存在一种场景,有一个主函数,需要复杂的运算和逻辑,所以为了后期的维护性,以及代码可读性,会拆分主函数,把一些运算单独抽离出来封装成函数,然后这些抽离出来的函数,仅仅是对这个主函数有用,在这种情况下,我们会把这些抽离出来的函数,写在主函数内部,可以避免全局对象污染。
这种写在函数内部的函数(嵌套函数),就称之为闭包函数,例如上述代码中的help
函数。
function a() {
var num = 10;
function b() { //闭包函数
console.log(num); //内部函数可以访问到外部函数的作用域
}
return b;
}
var fo = a(); //a函数执行,把a的执行结果赋给fo
fo(); //10
所谓闭包,就是这种外部环境能够访问内部函数作用域的变量的现象。
闭包的缺点是会造成内存泄露,可以理解为,本来a
函数执行完毕要释放内存,但是由于闭包,存放num
的内存不能被释放,那么如果有新的数据就不能存进来,就会被泄露。
24. 请完善下列代码,使控制台输出chris@123
。
function Person(name) {
this.name = name;
}
var person = new Person('chris');
person.run();
添加代码:
Person.prototype.run = function () {
console.log(`${this.name}@123`);
}
这里考察的是原型的问题。
25. 什么是原型和原型链?
- 原型
对象都有__proto__
这个属性,这个属性指向该对象的构造器的原型。
函数都有prototype
这个属性,这个属性指向该函数的原型,原型是一个对象格式。
var a = 123;
console.log(a.__proto__ === Number.prototype); //true
例如上述代码,a
的原型就是Number.prototype
。
要注意的是:
console.log(Function.__proto__ === Function.prototype); //true
console.log(Object.prototype.__proto__ === null); //true
- 原型链
当调用一个对象或者函数上的属性时,先去自身找,如果自身没有,去原型上找,如果原型上没有,去原型的原型上找,一直到找到,或者返回null
为止,形成了一种链式结构,称之为原型链。
26. 运行下列代码,控制台会输出什么?
var x = { foo: "A" };
x.constructor.prototype.foo = 'B';
var y = {};
console.log(x.foo);
console.log(y.foo);
输出结果为A
和B
。
27. 如果对象obj
包含一个非继承的名为propertyName
的属性,下面正确的是?
A. obj.doesPropertyExist('propertyName');
B. obj.hasProperty('propertyName');
C. obj.exists('propertyName');
D. obj.contains('propertyName');
E. obj.hasOwnProperty('propertyName');
正确选项为E。
28. 请选择下列正确选项。
function People(name) {
this.name = name;
}
var stu = new People("chris");
A. stu.__proto__ === People.prototype;
B. stu.prototype === People.__proto__;
C. People.__proto__ === Object.__proto__;
D. People.prototype === Object.prototype;
E. People.__proto__ === Function.prototype;
正确选项为A和E。
29. 简要说明javascript
事件循环机制。
- 执行同步代码。
- 遇到宏任务放到宏任务队列。
- 遇到微任务放到微任务队列。
- 同步代码执行完毕。
- 执行微任务队列,微任务队列执行完毕。
- 执行宏任务队列,宏任务队列执行完毕。
30. javascript
是如何处理异步的?
由于javascript
是单线程的,只能每次做一件事情,只有当前的事情做完,才可以做下一件事,因此javascript
提供了一些异步解决方案(回调、Promise
),来提高执行效率。
在javascript
中只有同步代码都执行完毕,执行栈为空的时候,才会执行异步代码。
异步有两个队列,微任务队列(relove
、reject
)和宏任务队列(setTimeout
),微队列执行优先于宏任务队列。
31. 请根据下列要求实现一个函数getSum
。
- 该函数接收两个参数。
- 第一个参数为长度不定的
arr
数组,且数组元素均为number
类型。 - 第二个参数为
number
类型的变量sum
如果数组arr
中的任意两个元素之和等于sum
,则返回true
否则返回false
。
function getSum(arr, sum) {
for (let index = 0; index < arr.length; index++) {
if(arr.indexOf(sum - arr[index]) !== -1 && arr.indexOf(sum - arr[index]) !== index){
return true;
};
}
return false;
}
var arr = [1,2,3,4,5];
var sum = 5;
getSum(arr, sum);
32. 请选择下列正确选项。
A. 除非抛出异常,否则foreach
方法无法终止。
B. typeof NaN === 'NaN'
C. javascript
中只有变量会提升,函数则不会。
D. 数组方法shift
会在数组头部添加一个新元素。
选择A选项。
33. 下列代码,调用output
时入参是什么?
var item;
var obj = {
name: 'chris',
email: 'chris@example.com',
sendMail: function () {}
}
for(item in obj) {
ouput(item);
}
A. 类型错误。
B. name, "email", "sendMail"
C. name, "email"
D. chris, chris@example.com, undefined
E. chris, chris@example.com, null
正确选项为B。
34. cookie
、localStorage
和sessionStorage
有什么区别?
- 每次
http
请求都会携带cookie
数据,而sessionStorage
和localStorage
不会。 -
cookie
存储的数据不能超过4KB
,sessionStorage
和localStorage
存储大小一般为5MB
或者更多。 -
cookie
只在设置的cookie
过期时间之前有效,localStorage
始终有效,窗口或浏览器关闭也一直保存。
sessionStorage
仅在当前浏览器窗口关闭之前有效。 -
sessionStorage
在不同的浏览器窗口中不共享,即使是同一个页面,localstorage
在所有同源窗口中都是共享的,cookie
在所有同源窗口中也是共享的。
35. 下列哪些关于cookie
的描述是正确的?
A. cookie
必须要在https
下传输,而不能在http
下传输。
B. cookie
的有效期,不能被设为浏览器关闭时自动删除。
C. cookie
是存储在客户端的。
D. 全部正确。
正确选项是D。
36. 简要描述javascript
语言的特性。
javascript
是一种解释型单线程弱类型的语言。
- 解释型
解释型语言可以通俗的理解为一个会中文的人读一本英文书籍,读到不会的地方,就去查中英字典,如果一个西班牙人来进行阅读,那么就使用中西字典。诸如javascript
、PHP
语言都是解释型序言。
- 解释型语言的优点:适应各种环境。
- 解释型语言的缺点:执行速度稍慢。
编译型语言可以通俗的理解为将一本英文书籍翻译成中文书籍,然后在进行阅读,但是如果此时一个西班牙人来阅读,还需要再次翻译成西语书籍才可以。诸如C
、C++
、Java
都是编译型语言。 - 编译型语言的优点:执行速度快。
- 编译型语言的缺点:难以适应各种环境。
- 单线程
单线程,可以理解为有一堆物品,但是只能由一个人运输。相反的,多线程,就是可以由多个人来运输这堆物品。
单线程会带来同步现象:当前的事情没有做完,就不能做下一件事,只能阻塞在这里,只有当前的事情做完,才可以做下一件事。但是,javascript
中会有一些异步的解决方案,来避免这些阻塞,从而提高单线程的执行效率。 - 弱类型
通俗的来说弱类型就是存放的数据类型灵活可变,但是与此同时,带来了数据不严谨的缺点。
反之,强类型就是存放的数据类型不可变,虽然不灵活,但是很严谨。
比如在javascript
中,可以让一个数字和字符串相加,结果会是一个新的字符串,javascript
会把数字转换成字符串类型,然后再相加。
37. 执行下列代码控制台的输出内容是什么?
let a = 5;
console.log(a);
console.log(a++);
console.log(a);
console.log(++a);
console.log(a);
a++
是先输出后运算,++a
是先运算后输出。
控制台输出内容为:5
、5
、6
、7
、7
38. javascript
有哪些数据类型?
- 基础类型
Number
、String
、Boolean
、undefined
、null
(注意,null
的typeof
输出类型为Object
)
通俗的讲,可以用占座的方式来理解undefined
和null
,当座位没被占的时候就是undefined
,当座位被占用了,但是占座的人还没来,这种情况是null
。 - 引用类型
Object
、Function
、Array
39. 函数字面量和函数表达式有什么区别?
console.log(test1); //test1函数体
console.log(test2); //not define
console.log(test3); //not define
function test1() {
console.log('test1');
}
(function test2() {
console.log('test2');
})
var a = function test3() {
console.log('test3')
}
test1(); //test1
console.log(window.test1); //test1函数体
test2(); //not define
console.log(window.test2); //undefined
a(); //test3
test3(); //not define
console.log(window.a); //test3函数体
console.log(window.test3); //undefined
- 函数字面量
上述代码的test1
函数就是通过函数字面量方式进行声明的,这种方法:
- 会让函数进行提升。
- 会污染全局对象。
- 函数表达式
上述代码中的test2
和test3
都是通过函数表达式的方式进行声明的,这种方法:
- 不会让函数进行提升。
- 不会污染全局对象。
40. 简单介绍函数内的变量声明。
function test() {
console.log(a); //undefined
console.log(b); //not define
var a = 1;
b = 2;
console.log(a); //1
console.log(b); //2
}
test();
console.log(a); //not define
console.log(b); //2
- 函数内用
var
声明的变量会被提升到函数体顶部var a;
。 - 函数内直接进行赋值的变量会成为全局对象上的一个属性。
- 函数体外部不能拿到函数体内部的变量。
41. 简单介绍return
关键字。
-
return
会结束函数的运行。 -
return
默认返回undefined
。
function test() {
return;
}
console.log(test()); //undefined
- 不手动添加
return
,函数会在末尾自动书写return
。
function test() {}
console.log(test()); //undefined
42. 简单介绍作用域。
- 全局作用域
var a = 1;
var b = 2;
function test() {
var c = 3;
}
可以理解为,直接在script
标签下声明的是处于全局作用域。
此时全局作用域中有:变量a
,变量b
,函数test
。
- 函数作用域
以上述代码为例,变量c
就处于test
函数作用域。 - 区别
- 全局作用域只能访问处于全局作用域的变量。
- 函数作用域可以访问函数作用域内的变量和外部作用域的变量。
43. 简单介绍this
关键字。
- 全局作用域
this
关键字指向window
对象。 - 函数作用域
const obj = {
foo: function () {
console.log(this);
}
}
obj.foo();
如果直接使用test()
这种方式调用函数,函数中的this
指向window
对象;如果像上述代码,this
指向obj
。
44. call
、bind
和apply
有什么区别?
const obj = {
name: "chris",
age: "18",
print(job){
console.log(`${this.name} + ${this.age} + ${job}`);
}
}
const newObj = {
name: "john"
}
obj.print() //chris + 18 + undefined
obj.print.call(newObj, "engineer"); //john + undefined + engineer
obj.print.apply(newObj, ["engineer"]); //john + undefined + engineer
obj.print.bind(newObj, "engineer")(); //john + undefined + engineer
45. 如何创建一个文档片段?
createdocumentfragment
方法创建了一虚拟的节点对象,也就是文档片段,也可理解为一个虚拟的父元素,将其他新创建的多个子元素添加到此父元素下,然后只需要通过一次操作将父元素添加到body
下即可时效最终效果,减少了对于DOM
的操作。
const d = document.createDocumentFragment();
const arrElement = new Array(10);
for (let index = 0; index < arrElement.length; index++) {
const element = document.createElement("input");
d.appendChild(element);
}
document.body.appendChild(d);
2. 进阶
1. 什么是防抖函数和截流函数?
- 防抖函数
直白来说,就是过一段时间以后再去执行。
例如:电梯门在有人通过之后,隔一段时间之后才会进行关门,如果此时又有人进入,继续等一段时间(重新计时)后再进行关门。例如某些搜索框也是如此,在输入完搜索内容后,过一段时间才会展示内容,如果在展示内容之前,又进行了输入,那么就会再次重新等待一段时间。
const input = document.getElementsByClassName('input')[0];
const handle = debunce(); //把return的返回函数给handle
input.oninput = (val) => {
handle(() => {console.log(val)});
//每次输入框输入时,就调用handle,并把打印val的函数传递过去
}
function debunce() {
let timer; //每次return函数都使用这个timer
return function (callback) { //这里callback接受了打印val的函数
clearInterval(timer); //清除定时器
timer = setTimeout(() => { //1s之后执行打印val函数
callback();
}, 1000);
}
}
如果在等待这1s
时,再次输入,就会清除上一次的定时器,不会进行打印,然后创建一个新的计时器,如果再次输入,就会重复流程,如果没输入,等待1s
后输出val
。
防抖函数使用了高阶函数(在函数内部返回一个新的函数),闭包(外部操作timer
),定时器来实现。
- 截流函数
- 规定时间内无论触发多少次,只在规定时间结束后执行一次。
const inputDom = document.getElementById('input');
const handle = throttle();
inputDom.oninput = (value => {
handle(value.data);
})
function throttle() {
let timer = null;
return function (value) {
if(timer){ //判断此时是否有定时器,如果有就直接return
return
}
timer = setTimeout(() => {
console.log(value)
timer = null;
}, 1000);
}
}
- 马上触发一次,然后规定时间结束后才会被第二次触发(利用时间戳)。
const inputDom = document.getElementById('input');
const handle = throttle();
inputDom.oninput = (value => {
handle(value.data);
})
function throttle() {
let time = null;
return function (value) {
if (!time || Date.now() - time > 1000) {
console.log(value);
time = Date.now(); //得到当前的时间戳
}
}
}
如果是一定要等某个动作结束后才触发,那么就使用防抖函数;
如果是规定时间内只能触发一次,那么就使用截流函数。
2. 如何进行性能优化?
- 减少http请求
http
请求文件需要时间,尤其在网速不好的情况下,会消耗大量时间,而且每一次请求都需要建立连接、释放连接,会对浏览器以及服务器产生性能消耗。
尽量合并js
和css
文件,这样就可以减少文件请求的数量。
减少对于图片的请求,使用css sprites
或者进行懒加载处理,或者对图片进行压缩。 - 减少重排和重绘
重绘:重绘仅仅是外观改变,例如背景颜色、文字颜色等改变,对性能影响不大。
重排:例如当元素的大小改变时,浏览器会重新渲染当前的元素,以及计算被该元素大小改变影响的元素,会造成比较多的性能消耗。
例如可以使用absolute
定位,这样当元素大小改变时不会影响其他元素,减少性能消耗。 - 减少DOM操作
操作DOM
会消耗比较可观的性能,我们可以这样理解:
DOM
是一座岛屿,javascript
是另一座岛屿,两者之间以一座桥梁连接,那么每次的DOM
操作都要通过这座桥梁。
DOM
操作是避免不了的,我们只能通过一些变通的办法来进行优化,例如查询DOM
时,可以赋值给变量,这样就不需要每次都进行查询。