前言
2020年已经到来,是不是该为了更好的2020年再战一回呢? ‘胜败兵家事不期,包羞忍耻是男儿。江东子弟多才俊,卷土重来未可知’,那些在秋招失利的人,难道就心甘情愿放弃吗!
此文总结2019年以来本人经历以及浏览文章中,较热门的一些面试题,涵盖从CSS到JS再到Vue再到网络等前端基础到进阶的一些知识。
总结面试题涉及的知识点是对自己的一个提升,也希望可以帮助到同学们,在2020年会有一个更好的竞争能力。
css篇- juejin.cn/post/684490…Es6篇- juejin.cn/post/684490…Vue篇- juejin.cn/post/684490…
Module Two - JavaScript
1 - 基本类型
- 简单数据类型(原始类型): String、Number、Boolean、Null、Undefined、Symbol
- 复杂数据类型(对象类型): Object
Null是对象吗?
虽然typeof null返回的是object,但这其实是JavaScript长久以来遗留的一个bug,null实质上是基本数据类型的一种
简单数据类型与复杂数据类型在数据存储上有什么区别?
简单数据类型以栈的形式存储,存储的是值
复杂数据类型以堆的形式存储,地址(指向堆中的值)存储在栈中。
❗ 小知识: 当我们把对象赋给另外一个变量时,复制的是地址,指向同一块内存空间,所以当其中一个对象改变时,另外一个对象也会随之改变
栈内存与对堆内存的区别
- 栈内存
JavaScript中原始类型的值被直接存储在栈中,在定义变量时,栈就为其分配好内存空间
- 存储的值大小固定
- 空间较小
- 可以直接操作其保存的变量,运行效率高
- 由系统自动分配存储空间
- 堆内存
JavaScript中引用类型(对象类型)的值实际存储在堆内存中,
它在栈中只存储了一个固定长度的地址,这个地址指向堆内存中的值
- 存储的值大小不定,可动态调整
- 空间较大,运行效率低
- 无法直接操作其内部存储,使用其地址读取值
- 通过代码分配空间
2 - typeof与instanceof的作用和区别是什么?
- typeof
- 能够正确判断简单数据类型(原始类型),除了null,typeof null结果为object
- 对于对象而言,typeof不能正确判断对象类型,typeof仅可以区分开function,除此之外,结果均为object
- instanceof
- 能够准确判断复杂数据类型,但是不能正确判断简单数据类型
instanceof的原理
instanceof是通过原型链进行判断的,A instanceof B,在A的原型链中层层查找,查找是否有原型等于B.prototype,如果一直找到A的原型链的顶端,即Object.prototype.__proto__,仍然不等于B.prototype,那么返回false,否则返回true
❗ 小知识:
var str = 'hello world' str instanceof String → false
var str = new String('hello world') str instanceof String → true
3 - 函数参数为对象的情况
function test(person) {
person.age = 26;
person = {
name: 'foo',
age: 30
}
return person
}
const p1 = {
name: 'bar',
age: 25
}
const p2 = test(p1)
console.log(p1) // name:bar age:26
console.log(p2) // name:foo age:30
解析 首先函数传参是按值传递的,即传递的是对象指针的副本,到函数内部修改参数这一步,p1的值也被修改,但是当我们重新为person分配一个对象时,是创建了一个新的地址(指针),也就和p1没有关系了,所以最终2个变量的值不同
4 - 类型转换
- 转Boolean
在条件判断中,除了undefined、null、''、false、0、-0、NaN,其他所有值都转为true,包括空数据和对象
- 对象转原始类型
对象在进行类型转换时,会调用内部的[[ToPrimitive]]函数
- 如果已经是原始类型,则不需要进行转换
- 调用x.value(),如果转换为基础类型,则返回转换的值
- 调用x.toString(),如果转换为基础类型,则返回转换的值
- 如果都不返回原始类型值,则报错
重写:
let a = {
valueOf(){
// toDo
},
toString(){
// toDo
},
[Symbol.toPrimitive](){
// toDo
}
}
❗ 小知识:
引用类型 → Number
先进行valueOf,再进行toString
引用类型 → String
先进行toString,再进行valueOf
若valueOf和toString都不存在,则返回基本类型错误,抛出TypeError
例子:
const Obj1 = {
valueOf:() => {
console.log('valueOf')
return 123
},
toString:() => {
console.log('toString')
return 'Chicago'
}
}
const Obj2 = {
valueOf:() => {
console.log('valueOf')
return {}
},
toString:() => {
console.log('toString')
return '{}'
}
}
console.log(Obj1 - 1) → valueOf 122
console.log(Obj2 - 1) → valueOf toString TypeError
- 加法运算
加法运算与其他有所区别
- 当运算符其中一方为字符串时,那么另一方也转换为字符串
- 当一侧为Number类型,另一侧为原始类型,则将原始类型转换为Number
- 当一侧为Number类型,另一侧为引用类型,则将引用类型和Number类型转换为字符串后拼接
例子:
1 + '1' → '11'
true + true → 2
4 + [1,2,3] → '41,2,3'
Ps: 特别注意 'a'+ +'b',因为 +'b' 会等于NaN,所以结果为 'aNaN'
- 除加法外,只要其中一方为数字,那么另一方就会转换为数字
- 比较运算符的转换规则( == )
- NaN:和其他类型比较永远返回false(包括自己)
- Boolean:和其他类型比较,Boolean首先被转化为Number(true → 1、false → 0)
- String和Number:String先转化为Number类型,再进行比较
- Null和undefined:null == undefined → true,除此之外,null、undefined和其他任何值比较均为false
- 原始类型和引用类型:引用类型转换为原始类型
5 - 常见面试点:[] == ![] 为何为true、[undefined]为何为false?
1)由于 ! 的优先级高于 == ,![] 首先会被转换为false,然后根据Boolean转换原则,false将会转换为Number类型,即转换为 0,然后左侧的 [] 转换为原始类型,也为 0 ,所以最终结果为 true
2)数组元素为null、undefined时,该元素被当作空字符串,所以 [undefined]、[null] 都会变为 0 , 最终 0 == false → true
6 - 什么是包装类型,与原始类型有什么区别??
包装类型即 Boolean、Number、String
与原始类型的区别:
true === new Boolean → false
123 === new Number('123') → false
'Chicago' === new String('Chicago') → false
typeof new String('Chicago') → Object
typeof 'Chicago' → string
什么是装箱和拆箱? 装箱即原始类型转换为包装类型、拆箱即包装类型转换为原始类型
如何使用原始类型来调用方法?
原始类型调用方法,实际上自动进行了装箱和拆箱操作
var name1 = 'Chicago'
var name2 = name1.substring(2)
以上2行代码,实际上发生了3个事情
- 创建一个String的包装类实例
- 在实例上调用substring方法
- 销毁实例
手动装箱、拆箱
var obj1 = new Number(123)
var obj2 = new String('chicago')
console.log(typeof obj1.valueOf()) → number
console.log(typeof obj2.toString()) → string
7 - 如何让 a == 1 && a == 2 && a == 3 为 true?
依据拆箱:
const a = {
value:[3,2,1],
valueOf:function(){
return this.value.pop()
} // 每次调用,删除一个元素
}
console.log(a == 1 && a == 2 && a == 3) // true (注意仅能判断一次)
★ 8 - 如何正确判断this? 箭头函数的this又是什么?
谁调用它,this就指向谁这句话可以说即精准又带坑
(绑定方式) 影响this的指向实际有4种:
- 默认绑定:全局调用
- 隐式调用:对象调用
- 显示调用:call()、apply()、bind()
- new绑定
- 默认
function foo(){
console.log(this.a)
}
var a = 2
foo() // 2 → this指向全局
- 隐式
function foo(){
console.log(this.a)
}
var obj1 = {
a = 1,
foo
}
var obj2 = {
a = 2,
foo
}
obj1.foo() // 1 → this 指向 obj1
obj2.foo() // 2 → this 指向 obj2
- 显式
function foo(){
console.log(this.a)
bar.apply( {a:2},arguments )
}
function bar(b){
console.log(this.a + b)
}
var a = 1 // 全局 a 变量
foo(3) // 1 5 → 1 说明第一个打印种 this 指向全局,5 说明第二个打印中 this 指向 {a:2}
❗ 小知识:
call()、apply()、bind()三者区别:
call()、apply()属于立即执行函数,区别在于接收的参数形式不同,前者是依次传入参数,后者参数可以是数组
bind()则是创建一个新的包装函数,并且返回,它不会立即执行bind(this,arg1,arg2···)
▲ 当call、apply、bind传入的第一个参数为 undefined/null 时,严格模式下this值为传入的undefined/null,非严格模式下,实际应用默认绑定,即指向全局(node环境下指向global、浏览器环境下指向window)
function info(){
console.log(this);
console.log(this.age);
}
var person = {
age:20,
info
}
var age = 28;
var info = person.info;
info.call(null); // window 、 28
- new绑定
- 构造函数返回值不是function/object
function Super(age){
this.age = age
}
let instance = new Super('26')
console.log(instance.age) // '26'
- 构造函数返回function/object
function Super(age){
this.age = age
let obj = {
a:2
}
return obj
}
let instance = new Super('26')
console.log(instance.age) // undefined → 返回的新obj中没有age
灵魂拷问:new 的实现原理
1 - 创建一个新对象
2 - 这个新对象会被执行[[原型]]链接
3 - 属性和方法被加入到this引用的对象里,并执行构造函数中的方法
4 - 如果函数没有返回其他对象,那么this指向这个新对象,否则this指向构造函数返回的对象
❗ 小知识:
对于this的绑定问题,优先级如下
New > 显式绑定 > 隐式绑定 > 默认绑定
箭头函数的this
1) 箭头函数没有自己的this
当我们使用箭头函数的时候,箭头函数会默认帮我们绑定外层this的值,所以在箭头函数中this的值与外层的this是一样的
例子1:
const obj = {
a: () => {
console.log(this)
}
}
obj.a() //打出来的是window
因为箭头函数默认不会使用自己的this,而是会和外层的this保持一致,最外层的this就是window对象
例子2:
let obj = {
age:20,
info:function(){
return () => {
console.log(this.age)
}
}
}
let person = { age:28 }
let info1 = obj.info()
info1() // 20
let info2 = obj.info.call(person)
info2() // 28
2) 箭头函数不能在call方法修改里面的this
函数的this可以通过call等显式绑定的方式修改,而为了减少this的复杂性,箭头函数无法用call()来指定this
const obj = {
a: () => {
console.log(this)
}
}
obj.a.call('123') //打出来的结果依然是window对象
9 - 如果对一个函数进行多次bind,会出现什么情况?
不管我们给函数进行几次bind显式绑定,函数中的this永远由 第一次bind 决定
let a = {}
let fn = function(){
console.log(this)
}
fn.bind().bind(a)() // => Window
10 - == 与 === 有什么区别?
- == 如果双方类型不同,会自动进行类型转换
- === 判断两者类型与值是否相同,不会进行类型转换
★ 11 - 何为闭包?
红宝书上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数
简单来说,闭包就是一个函数A内部有另一个函数B,函数B可以访问到函数A的变量和方法,此时函数B就是闭包
例子:
function A(){
let a = 1
window.B = function(){
console.log(a)
}
}
A() // 定义a,赋值window.B
B() // 1 → 访问到函数A内部的变量
闭包存在意义
在Js中,闭包存在的意义就是让我们可以间接访问到函数内部的变量 (函数内部变量的引用也会在内部函数中,不会因为执行完函数就被销毁,但过多的闭包会造成内存泄漏)
闭包的三个特性
- 闭包可以访问当前函数以外的变量
function getOuter(){
var data = 'outer'
function getDate(str){
console.log(str + data) // 访问外部变量 'data'
}
return getDate('I am')
}
getOuter() // I am outer
- 即使外部函数已经返回,闭包仍能访问外部函数定义的变量
function getOuter(){
var date = '815';
function getDate(str){
console.log(str + date); //访问外部的date
}
return getDate; //外部函数返回
}
var today = getOuter();
today('今天是:'); //"今天是:815"
today('明天不是:'); //"明天不是:815"
- 闭包可以更新外部变量的值
function updateCount(){
var count = 0;
function getCount(val){
count = val; // 更新外部函数变量
console.log(count);
}
return getCount; //外部函数返回
}
var count = updateCount();
count(815); //815
count(816); //816
经典面试题:循环中使用闭包解决var定义的问题(循环输出0-5,结果却是一堆6?)
for (var i = 0; i <= 5; i++) {
setTimeout(function () {
console.log(i)
}, 1000 * i)
} // 6,6,6,6,6,6
for(var i = 1;i <= 5; i++){
(function(j){
setTimeout( () => {
console.log(j)
},j * 1000)
})(i)
} // 0,1,2,3,4,5
// Tips:通过let定义i也能够解决,因为let具有块级作用域
12 - 浅拷贝?深拷贝?如何实现?
- 浅拷贝
1、首先可以通过Object.assign来实现,Object.assgin只会拷贝所有的属性值到新的对象中,但如果属性值是一个对象的话,拷贝的是地址,所以并不是深拷贝
let obj = {
a: 1,
b:{
foo:'foo',
bar:'bar'
}
}
let objCopy = Object.assign({},obj)
console.log(objCopy) // {a:1,b:{foo:'foo',bar:'bar'}}
obj.a = 2
console.log(objCopy.a) // 1 → 不会随着obj修改而修改
obj.b.foo = 'FOO'
console.log(objCopy.b) // {foo:'FOO',bar:'bar'} → 拷贝的是地址,指向同一个值,所以修改obj.b会影响到objCopy
2、也可以通过展开运算符...来实现浅拷贝
let obj = {
a: 1
b:{
foo:'foo',
bar:'bar'
}
}
let objCopy = { ...obj }
console.log(objCopy) // {a:1,b:{foo:'foo',bar:'bar'}}
obj.a = 2
console.log(objCopy.a) // 1
obj.b.foo = 'FOO'
console.log(objCopy.b) // {foo:'FOO',bar:'bar'}
与Object.assign一样,属性值为对象的拷贝,拷贝的是地址
- 深拷贝
通常来说浅拷贝可以解决大部分的问题,但如果遇到下面这种情况,就需要深拷贝来解决
let a = {
age:1,
jobs:{
first:'FE'
}
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native
浅拷贝只能解决第一层的问题,如果对象的属性还是对象的话,该属性两者会共享相同的地址,假如我们不想b的对象属性随a改变而改变,就需要通过深拷贝
1 - JSON.parse(JSON.stringify(object))
let a = {
age: 1,
jobs: { first: 'FE' }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
2 - lodash库中的cloneDeep()
13 - var、let、const的区别是什么?什么是变量提升?暂时性死区又是什么?
三者区别
1 - var【声明变量】
var 没有块的概念,可以跨块访问,无法跨函数访问
2 - let【声明块中的变量】
let 只能在块作用域里访问,不能跨块访问,更不能跨函数访问
3 - const【声明常量,一旦赋值便不可修改】
const 只能在块级作用域里访问,而且不能修改值
Tips: 这里的不能修改,并不是变量的值不能改动,而是变量所指向的那个内存地址保存的指针不能改动
❗ 小知识:
var a = 1
let b = 1
const c = 1
console.log(window.a) // 1
console.log(window.b) // undefined
console.log(window.c) // undefined
在全局作用域下使用let和const声明变量,变量并不会被挂载到window上,这一点与var不同
关于const,还有两个注意点:
- const声明之后必须马上赋值,否则报错
- const简单类型一旦声明就不能修改,而复杂类型(数组,对象)指针指向的地址不能修改,但内部数据可以修改
何为提升?
console.log(a) // undefined
var a = 1
上面两行代码,虽然在打印a前变量并没有被声明,但是却可以使用这个未声明的变量,不报错,这一种情况就叫做提升,而且提升的是声明
实际上,提升不仅仅只能作用于变量的声明,函数的声明也会被提升
console.log(a) // f a(){}
function a(){}
var a = 1
函数的声明优先级高于变量的声明
何为暂时性死区?
console.log(a) // ReferenceError: Cannot access 'a' before initialization
let a
为何这次就会报错呢? 只要一进入当前作用域,所要使用得变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量,这就是暂时性死区
var a = 123; // 声明
if (true) {
a = 'A'; // 报错 因为本块级作用域有a声明变量
let a; // 绑定if这个块级的作用域 不能出现a变量
}
对于暂时性死区,我的理解是声明提升了,但初始化没有被提升,而提升是声明提升,并初始化为undefined
总结
函数提升优于变量提升,函数提升会把整个函数提升到作用域顶部,变量提升只会把声明提升到作用域顶部
- var存在提升,我们能在声明之前使用。let和const由于暂时性死区的原因,不能在声明前使用
- var 在全局作用域下声明变量会导致变量被挂载到window上,其他两者不会
- let / const 作用基本一致,但后者不允许再次赋值
- let、const不允许在相同作用域内,重复声明同一个变量
★ 14 - 原型 / 构造函数 / 实例
- 原型 一个简单的对象,用于实现对象的属性继承
每一个JavaScript对象(null除外)在创建的时候就会有一个与之关联的对象,这个对象就是原型对象
每一个对象都会从原型上继承属性
- 构造函数 可以通过
new来创建一个对象的函数 - 实例 通过构造函数和
new创建出来的对象,便是实例
实例通过__proto__指向原型,通过constructor指向构造函数
以Object为例子,Object便是一个构造函数,我们通过它来构建实例
const instance = new Object()
这里,instance是Object的实例,Object是instance的构造函数,构造函数拥有一个prototype的属性来指向原型,因此有
const prototype = Object.prototype
原型、构造函数、实例 三者关系
实例.__proto__ === 原型
原型.constructor === 构造函数
构造函数.prototype === 原型
Tips:
const instance = new Object()
instance.constructor === Object // true
当获取 实例.constructor 时,其实实例上并没有constructor属性,当不能读到constructor属性时,会从实例的原型中读取
则有 instance.constructor === instance.__proto__.constructor
如果修改了instance.__proto__,instance.constructor将不再为Object
instance.__proto__ = null
instance.constructor === Object // false
★ 15 - 原型链
原型链是由相互关联的原型对象组成的链状结构
每个对象都有__proto__属性(构造函数也有),指向创建该对象的构造函数的原型 ,__proto__将对象链接起来组成了原型链。是一个可以用来实现继承和共享属性的有限链
原型链中的机制
- 属性查找机制 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象
Object.prototype(Object.prototype.__proto__ === null),假如还是没有找到,则输出undefined - 属性修改机制 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性,则需要通过
prototype属性(b.prototype.B = 1),但这样修改会导致所有继承于这个对象的实例的属性发生改变
16 - 原型如何实现继承?Class如何实现继承?Class本质是什么?
- 组合继承 使用原型链实现对原型属性和方法的继承,使用借用构造函数来实现对实例属性的继承
function Parent(value){
this.val = value // 实例属性
}
Parent.prototype.getValue = function(){ // 原型属性
console.log(this.val)
}
function Child(value){
Parent.call(this,value) // 借用构造函数来继承实例属性
}
Child.prototype = new Parent() // 原型链继承
const child = new Child(1)
child.getValue() // 1
child instancof Parent // true
这种方式优点在于构造函数可以传参,不会与父类共享引用属性,可以复用父类的函数,但缺点就是在继承父类函数的时候调用父类构造函数,导致子类的原型上会多了不需要的父类属性,存在内存浪费
- 寄生组合继承
function Parent(value){
this.val = value
}
Parent.prototype.getValue = function(){
console.log(this.val)
}
function Child(value){
Parent.call(this,value)
}
Child.prototype = Object.create(Parent.prototype,{
constructor:{
value:Child,
enumerable:false.
writable:true,
configurable:true
}
}) // 将父类的原型赋值给子类,并将原型的constructor修改为子类
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
这种寄生组合继承是对组合继承进行优化的,核心就是将父类的原型赋值给子类,并且将构造函数设置为子类,这样解决了无用的父类属性问题,还能正确的找到子类的构造函数
Class本质及继承实现
其实JavaScript中并不存在类的概念,class只是一种语法糖,本质来说还是函数
class Person{}
Person instanceof Function // true
Class继承
在ES6中,我们可以通过class实现继承
class Parent{
constructor(value){
this.val = value
}
getValue(){
console.log(this.val)
}
}
class Child extends Parent{
constructor(value){
super(value)
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
Class实现继承,核心在于使用extends关键字来表明继承自哪个父类,并且在子类构造函数中必须调用super
★ 17 - 什么是作用域和作用域链
说到作用域,我们要先理解什么是执行上下文
执行上下文 可以简单理解为一个对象,它具有三个类型:
- 全局执行上下文(caller)
- 函数执行上下文(callee)
eval()执行上下文
通过代码执行过程来理解执行上下文
- 创建 全局上下文(global EC)
- 全局执行上下文(caller)逐行以自上而下的顺序执行,遇到函数时,函数执行上下文(callee) 被
push到执行栈顶层 - 函数执行上下文被激活,成为
active EC,然后开始执行函数中的代码,全局执行上下文(caller)被挂起 - 函数执行完后,函数执行上下文(callee) 被
pop移除出执行栈,控制权交回给全局执行上下文(caller),继续按照自上而下的顺序执行代码
❗ 小知识:
变量对象,是执行上下文中的一部分,可以抽象为一种数据作用域
其实也可以理解为一个简单的对象,存储着该执行上下文中的所有变量和函数声明(不包括函数表达式)
活动对象(AO)- 当变量对象所处的上下文被激活时(active EC)时,称为活动对象
作用域
作用域可以理解为当前上下文中声明的变量和函数的作用范围,它规定了如何查找变量,也就是当前执行代码对变量的访问权限
作用域可以分为 块级作用域 和 函数作用域
作用域特性:
- 变量提升:一个声明在函数体内部都是可见的(仅var),函数声明由于变量声明
- 非匿名自执行函数 ,函数变量为 只读 状态,不能修改
let a = function(){
console.log(1)
};
(function a(){
a = 'a'
console.log(a)
})() // ƒ a() { a = 'a' ; console.log(a) }
作用域链
作用域链可以理解为一组对象列表,由当前环境与上层环境的一系列变量对象组成,因此我们可以通过作用域链访问到父级里面声明的变量或者函数
作用域链由两部分组成
[[scope]]属性:指向父级变量对象和作用域链,也就是包含了父级的[[scope]]和活动变量(AO)- 活动变量:自身活动变量
由于[[scope]]包含父级[[scope]]形成链状关系,便自上而下形成链式作用域
作用域链作用
保证当前执行环境里,有权访问的变量和函数是有序的(作用域链的变量只能向上访问变量,访问到window对象时被终止)
Ps:作用域链不允许向下访问
作用域链和原型继承查找时的区别 - 如果去查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined;但查找的属性在作用域链中不存在的话就会抛出ReferenceError
18 - script的引入方式
- HTML页面通过
<script>标签引入 - 异步方式
- Js动态插入
<script>标签 <script defer>- 这个属性的用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在
<script>元素中设置defer属性,相当于告诉浏览器立即下载,但延迟执行 - HTML5规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于DOMContentLoaded事件执行
- 这个属性的用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在
<script async>异步加载,但执行时会阻塞元素渲染- 同样与defer类似,async只适用于外部脚本文件,并告诉浏览器立即下载文件。但与defer不同的是,标记为async的脚本并不保证按照它们的先后顺序执行
- 第二个脚本文件可能会在第一个脚本文件之前执行。因此确保两者之间互不依赖非常重要。指定async属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容
- Js动态插入
19 - null 与 undefined 有什么区别?
- null 表示一个对象被定义了,但值是空值(定义为空)
- undefined 表示不存在这个值
typeof null // 'Object'
null 是一个空对象,没有任何的属性和方法
typeof undefined // 'undefined'
undefined 是一个表示'无'的原始值或表示缺少值,例如变量被声明了,但没有任何赋值时
从内存来看null和undefined,本质区别是什么?
- 给一个全局变量赋值为null,相当给这个属性分配了一块空的内存,然后值为null,Js会回收全局变量为null的对象,一般用于主动释放指向对象的引用
- 给一个全局变量赋值为undefined,相当于将这个对象的值清空,但是这个对象依旧存在,表示变量声明过但并未赋过值。 它是所有未赋值变量默认值
20 - 什么是内存泄漏?如何解决内存泄漏?
内存泄漏 在使用一些内存之后,如果后面不再需要用到这些内存,但没有将它们及时释放掉,就称为内存泄漏
如果出现严重的内存泄漏,那么有可能使得内存越来越大,最终导致浏览器崩溃
四种常见的Js内存泄露
- 意外的全局变量
未定义的变量会在全局对象创建一个新变量
function foo(arg){
bar = 'I am belong to global' // 未定义,会创建在全局中
}
解决方案:
- 在JavaScript头部加上`use strict`,使用严格模式避免意外的全局变量
- 被遗忘的定时器或回调函数
定义定时器(setTimeout / setInterval)后没有移除(clearTimeout / clearInterval)
- 脱离DOM的引用
- 闭包
闭包的关键是匿名函数可以访问父级作用域的变量,让变量不被回收
如果不及时清除,就会导致内存泄漏
如何解决内存泄漏?
通过GC垃圾回收机制来解决内存泄漏
所谓垃圾回收机制,是指找到内存空间中的垃圾并回收,可以再次利用这部分内存空间
垃圾回收机制有两种方式:
- 引用计数: 当声明一个变量并将一个引用类型赋值给该变量时,则这个值引用就加1,相反,如果包含这个值的变量又取得另外一个值,那么这个值的引用就减去1,当引用次数变为0,则说明没有办法访问这个值,所以就可以把其所占有的内存空间回收
- 标记清除: 当变量进入环境时,就标记这个变量为进入环境,当变量离开环境时就将其标记为离开环境
21 - Js中的循环方式有哪些?For in 与 For of 有什么区别?
在JavaScript中,我们可以采用多种方式实现循环
whiledo...whileforfor...infor...of
for in 与 for of 的区别
- for in
- 遍历对象及其原型链上可枚举的属性
- 如果用于遍历数组,除了遍历其元素外,还会遍历数组对象自定义的可枚举属性及其原型链上的可枚举属性
- 遍历对象返回的属性名和遍历数组返回的索引都是字符串索引
- 某些情况下,可能按随机顺序遍历数组元素
- for of
- es6 中添加的循环遍历语法
- 支持遍历数组,类数组对象(DOM NodeList),字符串,Map 对象,Set 对象
- 不支持遍历普通对象
- 遍历后输出的结果为数组元素的值
- 可搭配实例方法 entries(),同时输出数组的内容和索引
- 补充:Object.keys
- 返回对象自身可枚举属性组成的数组
- 不会遍历对象原型链上的属性以及 Symbol 属性
- 对数组的遍历顺序和 for in 一致
Tips: for in更适合遍历对象,尽量不用for in来遍历数组
22 - 关于数组(Array)API总结
迭代相关
- every()
对每一项运行给定函数,全true则返回true
- filter()
对数组中每一项运行函数,返回该函数会返回true项
- forEach()
对数组每一项运行函数,没有返回值 (forEach无法中途跳出forEach循环,break、continue和return都不奏效。)
- map()
对每一项运行函数,返回每次函数调用的结果组成的数组
- some()
对每一项运行函数,如果对任一项返回了true,则返回true
其他
- join('连接符')
通过指定连接符生成字符串
- push/pop
数组尾部推入和弹出,改变原数组,返回操作项
- shift/unshift
数组头部弹出和推入,改变原数组,返回操作项
- sort(fn)/reverse
数组排序(fn定义排序规则)与反转,改变原数组
- concat
连接数组,不改变原数组,返回新数组(浅拷贝)
- slice(start,end)
截断数组,返回截断后的新数组,不改变原数组
- splice(start,number,arg...)
从下标start开始,删除number个元素,并插入arg,返回所删除元素组成的数组,改变原数组
- indexOf / lastIndexOf(value, fromIndex)
查找数组元素,返回下标索引
- reduce / reduceRight(fn(prev, cur), defaultPrev)
归并数组,prev为累计值,cur为当前值,defaultPrev为初始值
23 - 常用字符串(String)API总结
- concat
连接字符串
- indexOf / lastIndexOf()
检索字符串、从后向前检索字符串
- match / replace / search
找到一个或多个正则表达式的匹配
替换与正则表达式匹配的子串
检索与正则表达式匹配的值
- slice
截取字符串片段,并在新的字符串中返回被截取的片段
- substr(start,length)
从起始索引号提取字符串中指定数目的字符
- substring(start,stop)
截取字符串中两个指定的索引号之间的字符。
- split
用于把一个字符串通过指定的分隔符进行分隔成数组
- toString()
返回字符串
- valueOf()
返回某个字符串对象的原始值
24 - map、filter、reduce各有什么作用
- map
作用:生成一个数组,遍历原数组,将每一个元素拿出来做一些变化后存入新数组
[1,2,3].map(item => item + 1) // [2,3,4]
另外map的回调接收三个参数,分别是当前元素、索引,原数组
常见题:['1','2','3'].map(parseInt) 结果是什么?
['1','2','3'].map(parseInt) → [1,NaN,NaN]
解析:
第一轮遍历 parseInt('1',0) // 1
第二轮遍历 parseInt('2',1) // NaN
第三轮遍历 parseInt('3',2) // NaN
- filter
作用:生成一个新数组,在遍历数组的时候将返回值为true的元素放在新数组
场景:我们可以利用这个函数删除一些不需要的元素(过滤)
let array = [1,2,3,4,5]
let newArray = array.filter(item => item !== 5)
console.log(newArray) // [1,2,3,4]
Tips:与map一致,也接收3个参数
- reduce
作用:将数组中的元素通过回调函数最后转换为一个值(归并)
场景:实现一个将数组里的元素全部相加得到一个值的功能
const arr = [1,2,3]
const sum = arr.reduce((acc,current) => {
return acc + current
},0)
console.log(sum) // 6
对于reduce来说,它只接受2个参数,分别是回调函数和初始值:
- 首先初始值为0,该值会在执行第一次回调函数时作为第一个参数传入
- 回调函数接收四个参数,分别为累计值、当前元素、当前索引、原数组
- 在第一次执行回调时,当前值和初始值相加为1,该结果会作为第二次回调时的累计值(第一个参数)传入
- 第二次执行时,相加的值分别为1和2,以此类推,循环结果后得出最终值
array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
- total 必选,初始值或计算结束后的返回值
- currentValue 必选,当前元素
- currentIndex 可选,当前索引
- arr 可选,当前元素所属的数组对象
- initialValue 可选,传递给函数的初始值
经典面试题 - 实现对象数组去重
25 - 代码复用
- 函数封装
- 继承
- 混入
mixin - 复制
extend - 借用
call/apply
26 - 并发与并行有什么区别?
- 并发 是宏观概念,指在一段时间内通过任务间的切换完成了这个两个任务,这种情况就称为并发
- 并行 是微观概念,同时完成多个任务的情况就称为并行
27 - require 与 import 有什么区别?
两者区别在于加载方式不同、规范不同
- 加载方式不同:require是在运行时加载,而import是在编译时加载
require('./a')() // 假设a模块是一个函数,立即执行a模块函数
var data = reuqire('./a').data // 假设a模块导出一个对象
Tips:require写在代码哪一行都可以
import Jq from 'jquery'
import * as _ from '_'
import {a,b,c} from './a'
import {default as alias, a as a_a, b, c} from './a';
Tips:import应该用在代码行开头
- 规范不同:require是CommonJS/AMD规范,而import属于ES6规范
❗ 小知识:
require特点:
- 提供了服务器/浏览器的模块加载方案。非语言层面的标准。
- 只能在运行时确定模块的依赖关系及输入/输出的遍历,无法进行静态优化
import特点:
- 语言规格层面支持模块功能。
- 支持编译时静态分析,动态绑定
28 - 如何判断一个变量是NaN?
NaN与任务值比较都是false,包括他自己,判断一个变量为NaN,可以通过isNaN()
isNaN(NaN) // true
29 - 严格模式有什么作用?表现在哪?
- 作用
- 消除JavaScript语法的一些不合理、不严谨之处,减少一些怪异行为
- 消除代码运行的一些不安全行为,保证代码运行的安全
- 提高编译器效率,增加运行速度
- 为未来新版本的JavaScript做好铺垫
- 表现
- 严格模式下,delete运算符后跟随非法标识符,会抛出语法错误
- 严格模式下,定义同名属性会抛出语法错误
- 严格模式下,函数形参存在同名,会抛出语法错误
- 严格模式下,不允许八进制整数直接量
- 严格模式下,arguments对象是传入函数内实参列表的静态副本
- 严格模式下,eval和arguments当做关键字,它们不能被赋值和用作变量声明
- 严格模式下,变量必须先声明,直接给变量赋值,不会隐式创建全局变量,不能用with
- 严格模式下,call/apply第一个参数为null/undefined,不会被转换为window
30 - 说说对松散类型的理解
JavaScript中的变量是松散类型,所谓松散类型,就是指当一个变量被声明出来就可以保存任何类型的值,一个变量所保存值的类型也可以改变
★ 31 - 函数防抖(debounce) 与 函数节流(throttle)
如果事件处理函数(click)调用频率无限制,会加重浏览器的负担,导致用户体验非常糟糕,那么我们可以采用debounce(防抖) 和 throttle(节流) 的方式来减少调用频率,同时又不影响实际效果
- 函数防抖(debounce) 当持续触发事件时,一定时间段内没有再次触发事件,事件处理函数才执行一次,如果设当的事件到来之前,又触发了事件,就重新开始延时(即设定的时间内触发事件将无效)
例如:
🔺当持续触发scroll事件时,事件处理函数handle只在停止滚动1000毫秒之后才会调用一次,即持续触发滚动事件的过程中,handle一直没有执行
function debounce(handle){
let timeout = null // 创建一个标记用来存放定时器
return function(){
clearTimeout(timeout) // 每当用户调用的时候把前一个定时器清空
timeout = setTimeout(() => {
handle.apply(this,arguments)
},500) // 500ms后触发,期间再次调用,则重新计算延时
}
}
function sayHi(){
console.log('防抖成功')
}
var btn = document.getElementById('button')
btn.addEventListener('click',debounce(sayHi))
- 函数节流(throttle)
当持续触发事件时,保证一定时间段内只调用一次事件处理函数 (通过判断是否到达一定条件来触发函数)
- 第一种方式:通过时间戳来判断是否已到可执行时间,记录上一次执行的时间戳,然后每次触发事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经到达设置的时间差,如果是则执行,并更新上次执行的时间戳
- 第二种方式:使用定时器
function throttle(handle){
let canRun = true // 通过闭包保存一个标记,不被回收
return function(){
if(!canRun) return // 在函数头部判断标记是否为true,为false时不允许调用handle
canRun = false // 设置标记为false
setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
handle.apply(this, arguments)
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
canRun = true
}, 500);
}
}
function sayHi() {
console.log('节流成功');
}
var btn = document.getElementById('button');
btn.addEventListener('click', throttle(sayHi)); // 节流
32 - 什么是事件捕获?什么是事件冒泡?
- 事件捕获:事件从最不精准的目标(document对象)开始触发,然后到最精确的目标 (不精确 → 精确)
- 事件冒泡:事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发 (特定 → 不特定)
哪些事件不支持冒泡?
- 鼠标事件:mouseleave、mouseenter
- 焦点事件:blur、focus
- UI事件:scroll、resize
- ···
事件如何先冒泡后捕获?
对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件时,先暂停执行,直到冒泡事件被捕获后再执行事件
33 - 如何阻止事件冒泡?又如何阻止默认行为?
- 阻止事件冒泡
非IE浏览器:event.stopPropagation()
IE浏览器:window.event.cancelBubble = true
function stopBubble(e){
// 如果提供了事件对象,则是非IE浏览器下
if(e && e.stopPropagation){
// 因此它支持W3C的stopPropagation()方法
e.stopPropagation()
}else{
// IE浏览器下,取消事件冒泡
window.event.cancelBubble = true
}
}
- 阻止默认行为
非IE浏览器:event.preventDefault()
IE浏览器:window.event.returnValue = false
function stopDefault(e) {
//阻止默认浏览器动作(W3C)
if (e && e.preventDefault) e.preventDefault();
//IE中阻止函数器默认动作的方式
else window.event.returnValue = false;
return false;
}
34 - 事件委托是什么?
所谓事件委托,就是利用事件冒泡的原理,让自己所触发的事件,让其父元素代替执行
即:不在事件(直接DOM)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生在哪一个子元素上来做出不同的响应
为什么要用事件委托?好处在哪?
- 提高性能
<ul>
<li>苹果</li>
<li>香蕉</li>
<li>凤梨</li>
</ul>
// 在ul上设置监听函数(Good)
document.querySelector('ul').onclick = (event) => {
let target = event.target
if (target.nodeName === 'LI') {
console.log(target.innerHTML)
}
}
// 在每一个li上监听函数(Bad)
document.querySelectorAll('li').forEach((e) => {
e.onclick = function() {
console.log(this.innerHTML)
}
})
- 新添加的元素也能触发绑定在父元素上的监听事件
事件委托与事件冒泡的对比
- 事件冒泡:父元素下无论是什么元素,点击后都会触发 box 的点击事件
- 事件委托:可以对父元素下的元素进行筛选
35 - Js中高阶函数是什么?
高阶函数(Highter-order-function)的定义很简单,就是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入
- 输出一个函数
也就是说高阶函数是对其他函数进行操作的函数,可以将它们作为参数传递,或者是返回它们。
- 函数作为参数传递
Javascript中内置了一些高阶函数,比如
Array.prototype.map、Array.prototype.filter、Array.prototype.reduce···,它们接受一个函数作为参数,并应用这个函数到列表的每一个元素
对比使用高阶函数和不使用高阶函数
例子:有一个数组[1,2,3,4],我们想要生成一个新数组,其元素是之前数组的两倍
- 不使用高阶函数
const arr1 = [1,2,3,4]
const arr2 = []
for(let i = 0; i < arr1.length; i++){
arr2.push(arr1[i] * 2)
}
- 使用高阶函数
const arr1 = [1,2,3,4]
const arr2 = []
arr2 = arr1.map( item => item * 2)
- 函数作为返回值输出
在判断类型的时候可以同个Object.prototype.toString.call来获取对应对象返回的字符串,如:
let isString = obj => Object.prototype.toString.call( obj ) === '[object String]'
let isArray = obj => Object.prototype.toString.call( obj ) === '[object Array]'
let isNumber = obj => Object.prototype.toString.call( obj ) === '[object Number]'
可以发现这三行有许多重复代码,只需要把具体的类型抽离出来就可以封装成一个判断类型的方法,如:
let isType = (type) => { return (obj) => {
console.log(Object.prototype.toString.call(obj))
return Object.prototype.toString.call(obj) === '[object ' + type + ']'
}
}
isType('String')('123'); // true
isType('Array')([1, 2, 3]); // true
isType('Number')(123); // true
36 - 什么是柯里化函数?
柯里化 - 简单来说就是只传递函数一部分参数来调用它,让它返回一个新函数去处理剩下的参数。
通过add()函数来了解柯里化
const add = (...args) => args.reduce( (a, b) => a + b ) // a为初始值或计算结束的返回值,b为当前元素
// 传入多个参数,执行add函数
add(1,2) // 3
// 假设我们实现了一个柯里化函数,支持一次传入一个参数
let sum = currying(add)
// 封装第一个参数,方便重用
let addCurryOne = sum(1)
addCurryOne(2) // 3
addCurryOne(3) // 4
实现currying函数
我们可以理解所谓的柯里化函数,就是封装一系列的处理步骤,通过闭包将参数集中起来计算,最后再把需要处理的参数传进去,那么如何实现currying函数呢?
实现原理就是用闭包把传入的参数保存起来,当传入参数的数量足够执行函数时,就开始执行函数
实现一个健壮的currying函数:
function currying(fn, length) {
length = length || fn.length // 第一次调用获取函数 fn 参数的长度,后续调用获取 fn 剩余参数的长度
return function( ...args ) { // currying 包裹之后返回一个新函数,接收参数为 ...args
return args.length >= length // 新函数接收的参数长度是否大于等于 fn 剩余参数需要接收的长度
? fn.apply(this, args) // 满足要求,执行 fn 函数,传入新函数的参数
: currying(fn.bind(this, ...agrs), length - args.length) // 不满足要求,递归 currying 函数,新的 fn 为 bind 返回的新函数(bind 绑定了 ...args 参数,未执行),新的 length 为 fn 剩余参数的长度
}
}
// Test
const fn = currying(function(a,b,c) {
console.log([a, b, c])
})
fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]
实际应用
- 延迟计算:部分求和、bind函数
延迟计算:
const add = (...args) => args.reduce((a, b) => a + b);
// 简化写法
function currying(func) {
const args = [];
return function result(...rest) {
if (rest.length === 0) {
return func(...args);
} else {
args.push(...rest);
return result;
}
}
}
const sum = currying(add);
sum(1,2)(3); // 未真正求值
sum(4); // 未真正求值
sum(); // 输出 10
- 动态创建函数:添加监听addEvent、惰性函数
- 参数复用
37 - 一行代码将数组扁平化并去重,最终得到升序不重复的数组
Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b}
解析:
array.from - 从一个类数组或可迭代对象创建一个新的,浅拷贝的数组实例
array.flat - 用于将嵌套的数组'拉平'(扁平化)
[1,2,[3,4]].flat() // [1,2,3,4]
array.sort - 用于对数组的元素进行排序
- 该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。
比较函数应该具有两个参数 a 和 b,其返回值如下:
- 若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。
- 若 a 等于 b,则返回 0
- 若 a 大于 b,则返回一个大于 0 的值。
1 - arr.flat(Infinity) 先将数组扁平化
2 - new Set(arr.flat(Infinity)) 去重扁平化后的数组
3 - Array.from(new Set(arr.flat(Infinity))) 创建一个新的,浅拷贝的数组实例
4 - Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b } 将该新数组进行升序排序
38 - JS如何动态添加、移除、移动、复制、创建和查找节点?
- 创建新节点
createDocumentFragment() - 创建一个DOM片段
createElement() - 创建一个具体的元素
createTextNode() - 创建一个文本节点
- 添加、移除、替换、插入
appendChild() - 添加子节点
removeChild() - 移除子节点
replaceChild() - 替换子节点
insertBefore() - 插入
- 查找
getElementsByTagName() - 通过标签名称
getElementsByName() - 通过元素的Name属性
getElementById() - 通过元素id,具有唯一性
39 - Javascript是一门怎样的语言?有什么特点?
- 脚本语言:
Javascript是一种解释型语言,C、C++等语言先编译后执行,而Javascript是在程序的运行过程中逐行进行解释 - 基于对象:
Javascript是一种基于对象的脚本语言,它不仅可以创建对象,也能使用现有的对象 - 简单:
Javascript语言中采用的是弱类型的变量类型,对使用的数据类型未做出严格的要求,是基于Java基本语句和控制的脚本语言 - 动态性:
Javascript是一种采用事件驱动的脚本语言,它不需要经过Web服务器就可以对用户的输入做出响应 - 跨平台性:
Javascript不依赖于操作系统,仅需要浏览器的支持
40 - 兼容各种浏览器版本的事件绑定
/**
* 兼容低版本IE,element为需要绑定事件的元素,
* eventName为事件名(保持addEventListener语法,去掉on),fun为事件响应函数
*/
function addEvent(element, eventName, fun){
if(element.addEventListener){
element.addEventListener(eventName, fun, false)
}else{
element.attachEvent('on' + eventName, fun)
}
}
41 - sort()排序原理是什么?
sort()内部是利用递归进行冒泡排序的
- 解析冒泡排序的原理
- 比较相邻的元素,如果第一个比第二个大,就交换它们两个
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,在这一点,最后的元素应该会是最大的数
- 针对所有的元素重复以上的步骤,除了最后一个
- 持续每次对越来越少的元素重复上面的步骤,知道没有任何一对数字需要比较
- 示例
var arr = [1,5,4,2]
sort()的比较逻辑为:
- 1和5比,1和4比,1和2比
- 5和4比,5和2比
- 4和2比
- sort()排序规则
- return > 0 则交换数组相邻2个元素的位置
- arr.sort(function(a,b){ ... })
- a → 代表每一次执行匿名函数时,找到数组中的当前项
- b → 代表当前项的后一项
1 - 升序
var arr = [45, 42, 10, 147, 7, 65, -74]
console.log(arr.sort()) → 默认法 缺点:默认排序顺序是根据字符串UniCode码。因为排序是按照字符串UniCode码的顺序进行排序的(按首位排序)
// [-74, 10, 147, 42, 45, 65, 7]
console.log(
arr.sort(function(a, b) {
return a - b; // 若return返回值大于0(即a>b),则a,b交换位置
}) → 指定排序规则,return可返回任何值
)
// [-74, 7, 10, 42, 45, 65, 147]
2 - 降序
var arr = [45, 42, 10, 111, 7, 65, -74];
console.log(
arr.sort(function(a, b) {
return b - a; // 若return返回值大于零(即b>a),则a,b交换位置
}) → 指定排序规则,return可返回任何值
);
42 - 如何判断当前脚本运行在浏览器还是node环境中?
通过判断Global对象是否为window,如果不为window,则当前脚本没有运行在浏览器中
43 - 一行代码求数组最大值与最小值
var a = [1, 2, 3, 5];
alert(Math.max.apply(null, a)); //最大值
alert(Math.min.apply(null, a)); //最小值
之所以需要用到apply,是因为 Math.max / Math.min 不支持传递数组过去
44 - offsetWidth/offsetHeight,clientWidth/clientHeight 与 scrollWidth/scrollHeight 的区别
-
offsetWidth → 返回元素的宽度(包括元素宽度、内边距和边框,不包括外边距)
-
offsetHeight → 返回元素的高度(包括元素高度、内边距和边框,不包括外边距)
-
clientWidth → 返回元素的宽度(包括元素宽度、内边距,不包括边框和外边距)
-
clientHeight → 返回元素的高度(包括元素高度、内边距,不包括边框和外边距)
-
style.width → 返回元素的宽度(包括元素宽度,不包括内边距、边框和外边距)
-
style.height → 返回元素的高度(包括元素高度,不包括内边距、边框和外边距)
-
scrollWidth → 返回元素的宽度(包括元素宽度、内边距和溢出尺寸,不包括边框和外边距),无溢出的情况,与clientWidth相同
-
scrollHeigh → 返回元素的高度(包括元素高度、内边距和溢出尺寸,不包括边框和外边距),无溢出的情况,与clientHeight相同
45 - offsetTop / offsetLeft / scrollTop / scrollLeft 的区别
-
offsetTop → 返回元素的上外缘距离最近采用定位父元素内壁的距离,如果父元素中没有采用定位的,则是获取上外边缘距离文档内壁的距离。
-
offsetLeft → 此属性和offsetTop的原理是一样的,只不过方位不同
-
scrollLeft → 此属性可以获取或者设置对象的最左边到对象在当前窗口显示的范围内的左边的距离,也就是元素被滚动条向左拉动的距离。
-
scrollTop → 此属性可以获取或者设置对象的最顶部到对象在当前窗口显示的范围内的顶边的距离,也就是元素滚动条被向下拉动的距离。
46 - Javascript中的arguments对象是什么?
在函数调用的时候,浏览器每次都会传递进两个隐式参数,一个是函数的上下文对象this,另外一个则是封装实参的伪数组对象arguments
关于arguments
arguments定义是对象,但是因为对象的属性是无序的,而arguments是用来存储实参的,是有顺序的,它具有和数组相同的访问性质及方式,并拥有数组长度属性length(类数组对象、用来存储实际传递给函数的参数)arguments访问单个参数的方式与访问数组元素的方式相同,例如arguments[0]、arguments[1]、arguments[n],在函数中不需要明确指出参数名,就能访问它们。通过length属性可以知道实参的个数。arguments有一个callee属性,返回正被执行的Function对象
function fun() {
console.log(arguments.callee === fun); // true
}
fun();
- 在正常模式下,
arguments对象是允许在运行时进行修改
function fun() {
arguments[0] = 'sex';
console.log(arguments[0]); // sex
}
fun('name', 'age');
一行代码实现伪数组arguments转换为数组
var args = [].slice.call(arguments)
★ 47 - Js的事件循环(Event Loop)机制
为什么Js是单线程?
Javascript作为主要运行在浏览器的脚本语言,主要用途之一就是操作Dom
如果Javascript同时有两个线程,同时对同一个Dom进行操作,这时浏览器应该听哪个线程的,又如何判断优先级?
为了避免这种问题,Javascript必须是一门单线程语言
执行栈与任务队列
由于Javascript是单线程语言,当遇到异步任务(如Ajax)时,不可能一直等到异步执行完成后,再继续往下执行,因为这段时间浏览器处于空闲状态,会导致巨大的资源浪费
执行栈
当执行某个函数、事件(指定过回调函数)时,就会进入执行栈中,等待主线程读取
执行栈可视化:
主线程
主线程与执行栈不同,主线程规定了现在执行执行栈中的哪个事件
主线程循环: 即主线程会不停的从执行栈中获取事件,然后执行完所有栈中的同步代码
当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,这个队列称为任务队列TaskQueue
当主线程将执行栈中的所有代码都执行完后,主线程将会去查看任务队列中是否存在任务。 如果存在,那么主线程会依次执行那些任务队列中的回调函数
Javascript异步执行的运行机制
- 所有任务都在主线程上执行,形成一个执行栈
- 主线程之外,还存在一个任务队列(
TaskQueue)。只要异步任务有了返回结果,就在任务队列之中放置一个事件 - 当执行栈中的所有同步任务执行完毕,就会去查看任务队列,那些对应的异步任务,结束等待状态,进入执行栈并开始执行
- 主线程会不断的重复第三点
宏任务与微任务
异步任务可以分为两类,不同类型的API注册的任务会依次进入到各自对应的队列中,然后等待事件循环(EventLoop)将它们依次压入执行栈中执行
- 宏任务
MacroTask
script(整体代码),setTimeout,setInterval,setImmediate,UI渲染,I/O流操作,postMessage,MessageChannel
- 微任务
MicroTask
Promise,MutaionObserver,process.nextTick
事件循环(EventLoop)
Event Loop(事件循环)中,每一次循环称为 tick, 每一次tick的任务如下:
- 执行栈选择最先进入队列的宏任务(通常是script整体代码),如果有则执行
- 检查是否存在 Microtask,如果存在则不停的执行,直至清空 microtask 队列
- 更新render(每一次事件循环,浏览器都可能会去更新渲染)
- 重复以上步骤
综上所述
宏任务 → 所有微任务 → 下一个宏任务
两道题检验是否已经 get√
题 1:
setTimeout(function () {
console.log(1)
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val)
})
console.log(4)
Result:
2 → 4 → 3 → 1
题 2:
new Promise(resolve => {
resolve(1);
Promise.resolve().then(() => {
// t2
console.log(2)
});
console.log(4)
}).then(t => {
// t1
console.log(t)
});
console.log(3);
Result:
4 → 3 → 2 → 1
解析:
- script任务运行,首先遇到Promise实例,执行构造函数,输出4,此时微任务队列中有t2和t1
- script任务继续执行同步代码,输出3后第一个宏任务执行完成
- 执行所有的微任务,即输出2和1
??? 为什么t2会比t1先执行 ???
- 根据 Promises/A+ 规范
- Promise.resolve 方法允许调用时不带参数,直接返回一个resolved 状态的 Promise 对象
- 立即 resolved 的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时
48 - 异步编程的六种方式
Javascript是单线程工作,也就是只有一个脚本执行完之后才可以执行下一个脚本,两个脚本不能同时执行,那么如果脚本耗时很长,后面的脚本都必须排队等待,会拖延整个程序的执行
异步编程的六种方式
- 回调函数 - 假如f1是一个需要一定时间的函数,所以可以将f2写成f1的回调函数,将同步操作变成操作,f1不会阻塞程序的运行,f2也不需等待
function f1(cb){
setTimeout(() => {
console.log('f1')
})
cb()
}
function f2(){
console.log('f2')
}
f1(f2) // f2 → f1
function fn(a,b,cb){
var num = Math.ceil(Math.random() * (a - b) + b)
cb(num)
}
fn(10,20,function(num){
console.log("随机数" + num);
}) // 10 ~ 20 的随机数
总结:
- 回调函数易于实现,便于理解,但是多次回调会导致代码高度耦合
- 回调函数定义:函数A作为参数(函数引用)传递到另外一个函数B,并且这个函数B执行函数A,我们就叫函数A叫做回调函数,如果没有名称(函数表达式),我们就叫它匿名回调函数
- 回调函数优点:简单,容易理解
- 回调函数缺点:不利于代码的阅读和维护,各部分之间高度耦合,而且每一个任务只能指定一个回调函数
- 事件监听 - 采用事件驱动模式,脚本的执行不取代代码的顺序,而取决于某一个事件是否发生
监听函数有:on、bind、listen、addEventListener、observe
优点:容易理解,可以绑定多个事件,每一个事件可以接收多个回调函数,而且可以减少耦合,利于模块化
缺点:整个程序都要变成事件驱动型,运行流程会变得不清晰
element.onclick = function(){
// toDo
}
Or:
element.onclick = handler1
element.onclick = handler2
element.onclick = handler3
缺点:
当同一个element元素绑定多个事件时,只有最后一个事件会被添加,上述只有handler3会被添加执行
elment.attachEvent("onclick", handler1)
elment.attachEvent("onclick", handler2)
elment.attachEvent("onclick", handler3)
Result: 3 → 2 → 1
elment.addEventListener("click", handler1, false)
elment.addEventListener("click", handler2, false)
elment.addEventListener("click", handler3, false)
Result:1 → 2 → 3
(PS:该方法的第三个参数是泡沫获取,是一个布尔值:当为false时表示由里向外,true表示由外向里。)
DOM - addEventListener()和removeListener()
addEventListenner()和removeListenner()表示用来分配和删除事件的函数。这两种方法都需要三种参数,分别为:
- 事件名称(String)
- 触发事件的回调函数(function)
- 指定事件的处理函数的时期或阶段(boolean)
- 观察者模式(Observe) - 也称为发布订阅模式
定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这一个主题对象一旦发生状态变化,就会通知所有观察者对象,使得它们能够自动更新自己
优点:
- 支持简单的广播通信,自动通知所有已经订阅过的对象
- 页面载入后,目标对象很容易与观察者存在一种动态关联,增加灵活性
- 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用
- Promise
- promise对象是commonJS工作组提出的一种规范,一种模式,目的是为了异步编程提供统一接口
- promise是一种模式,promise可以帮忙管理异步方式返回的代码。他将代码进行封装并添加一个类似于事件处理的管理层。我们可以使用promise来注册代码,这些代码会在在promise成功或者失败后运行
- promise完成之后,对应的代码也会执行。我们可以注册任意数量的函数再成功或者失败后运行,也可以在任何时候注册事件处理程序
- promise有两种状态:1、等待(pending);2、完成(settled)
- promise会一直处于等待状态,直到它所包装的异步调用返回/超时/结束
- 这时候promise状态变成完成。完成状态分成两类:1、解决(resolved);2、拒绝(rejected)
- promise解决(resolved):意味着顺利结束。promise拒绝(rejected)意味着没有顺利结束
- Generator - Generator函数是一个状态机,封装了多个内部状态
- async
49 - 同源策略
Javascript只能与同一个域中的页面进行通讯
两个脚本被认为是同源的条件:
- 协议相同
- 端口相同
- 域名相同
50 - jsonP的优缺点
- 优点
- 它不像
XMLHttpRequest对象实现的AJAX请求那样受到同源策略的限制,jsonP可以实现跨越同源策略 - 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要
XMLHttpRequest或ActiveX的支持 - 在请求完毕后可以通过调用
callback的方式回传结果。将回调方法的权限给了调用方。
- 它不像
- 缺点
- 它只支持
GET请求而不支持POST等其他类型的HTTP请求 - 它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用问题
- JsonP在调用失败的时候不会返回各种http状态码
- 缺点是安全性。万一假如提供JsonP的服务存在页面注入漏洞(即它所返回的javascript的内容被人控制),那么结果是什么?所有调用这个JsonP的网站都会存在漏洞,缺乏安全。
- 它只支持
★ 51 - AJAX
什么是AJAX,为什么使用AJAX?
AJAX是一种创建交互式网页应用的网页开发技术AJAX可以实现在不必刷新整个页面的情况下实现局部更新,与服务器进行异步通讯的技术
XMLHttpRequest对象
XMLHttpRequest对象可以说是AJAX的核心对象,是一种支持异步请求的技术。即XMLHttpRequest使你可以使用javascript向服务器提出请求并做出响应,又不会导致阻塞用户。通过XMLHttpRequest对象,可以实现在不刷新整个页面的情况下实现局部更新
XMLHttpRequest对象的常见属性
onreadystatechange- 一个Js函数对象,当readyState属性改变时会调用它(请求状态改变的事件触发器)readyState- Http请求的状态,当一个XMLHttpRequest初次创建时,这个属性的值从0开始,直到接收到完整的Http响应,这个值增加到4- 0 - 初始化状态。
XMLHttpRequest对象已创建或已被abort()方法重置。 - 1 -
open()方法已调用,但是send()方法未调用。请求还没有被发送 - 2 -
send()方法已调用,HTTP 请求已发送到 Web 服务器,但未接收到响应 - 3 - 所有响应头部都已经接收到,响应体开始接收但未完成
- 4 - Http响应已经完全接收
- 0 - 初始化状态。
readyState 的值不会递减,除非当一个请求在处理过程中的时候调用了 abort() 或 open() 方法
每次这个属性的值增加的时候,都会触发 onreadystatechange 事件句柄。
status- 由服务器返回的 HTTP 状态代码,如 200 表示成功,而 404 表示 "Not Found" 错误。当 readyState 小于 3 的时候读取这一属性会导致一个异常。
关于Http状态码,常见如下:
1) 1XX 通知
2) 2XX 成功
3) 3XX 重定向
4) 4XX 客户端错误
5) 5XX 服务端错误
最基本的响应状态:
- 200('ok') : 服务器已成功处理了请求
- 400('bad request'):服务器无法解析该请求
- 500('Internal Server Error'):服务器内部错误服务器遇到错误,无法完成请求
- 301('Moved Permanently'):永久移动请求的网页已永久移动到新位置,即永久重定向
- 404('Not Found'):未找到服务器找不到请求的网页
- 409('Conflict'):服务器在完成请求时发生冲突
XMLHttpRequest对象的常见API
Open()- 创建http请求- 第一个参数:定义请求的方式(get/post)
- 第二个参数:提交的地址
url - 第三个参数:指定异步/同步(true → 异步,false → 同步)
- 第四第五个参数:http认证
在一个已经激活的request下(已经调用open()或者openRequest()方法的请求)再次调用这个方法相当于调用了abort()方法。
setRequestHeader()- 向一个打开但未发送的请求设置或添加一个Http请求(设置请求头)- 第一个参数:将要被赋值的请求头名称(header)
- 第二个参数:给指定的请求头赋值(value)
send()- 发送http请求,使用传递给open()方法的参数,以及传递给该方法的可选请求体- 如果为get,参数为null / 如果为post,参数为提交的参数
abort()- 取消当前响应getAllResponseHeaders()- 把Http响应头部作为未解析的字符串返回getResponseHeader()- 返回指定的 HTTP 响应头部的值- 其参数是要返回的 HTTP 响应头部的名称。可以使用任何大小写来制定这个头部名字,和响应头部的比较是不区分大小写的
AJAX的流程是怎么样的?
- 创建
XMLHttpRequest对象 - 定义
Http对象 - 可以设置
Http请求的请求头 - 设置响应状态改变的事件回调函数
- 发送请求
- 获取异步调用返回的数据
- 使用Js和DOM进行局部解析
原生实现一个Ajax
var ajax = {}
// 兼容性创建httpRequest
ajax.httpRequest = function(){
// 判断是否支持XMLHttpRequest
if(window.XMLHttpRequest){
return new XMLHttpRequest()
}
// 兼容 Ie
var versions = [
"MSXML2.XmlHttp.6.0",
"MSXML2.XmlHttp.5.0",
"MSXML2.XmlHttp.4.0",
"MSXML2.XmlHttp.3.0",
"MSXML2.XmlHttp.2.0",
"Microsoft.XmlHttp"
]
// 定义局部xhr,存储Ie浏览器的ActiveXObject对象
var xhr
for (var i = 0; i < versions.length; i++) {
try {
xhr = new ActiveXObject(versions[i]);
break;
} catch (e) {
}
}
return xhr
}
ajax.send = function(url, callback, method, data, async){
// 默认异步
if(async === undefined){
async = true
}
var httpRequest = ajax.httpRequest()
// 创建Http请求(open)
httpRequest.open(method, url, async)
// 请求状态改变的事件触发器
httpRequest.onreadystatechange = function(){
// readyState变为4时,从服务器拿到数据
if(httpRequest.readyState === 4){
callback(httpRequest.responseText)
}
}
// 设置http请求的请求头(setRequestHeader)
if (method == 'POST') {
//给指定的HTTP请求头赋值
httpRequest.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
}
// 发送Http请求
httpRequest.send(data)
}
// 封装GET/POST请求
ajax.get = function (url, data, callback, async) {
var query = [];
for (var key in data) {
query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
}
ajax.send(url + (query.length ? '?' + query.join('&') : ''), callback, 'GET', null, async)
}
ajax.post = function (url, data, callback, async) {
var query = [];
for (var key in data) {
query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
}
ajax.send(url, callback, 'POST', query.join('&'), async)
}
52 - 如何解决跨域问题?
- CORS → 服务端设置请求头(
Access-Control-Allow-Origin) - jsonP → 动态加载
<script>标签(只能解决GET请求) - window.name → 利用浏览器窗口内,载入所有的域名都是共享一个window.name
- ducment.domain / window.postMessage()
53 - document.Ready()、onload、前写JS有什么区别?
document.Ready()可以定义多个函数,按照绑定的顺序进行执行,而onload只能执行一次,定义多个onload,后面的会覆盖前面的onload()是在页面所有元素都加载完后才执行document.Ready()是在DOM绘制完就执行,不必等到完全加载完再执行</body>前写Js则是运行到就开始执行,不管有没有加载完成,所以有可能出现Js操作节点时获取不到该节点
54 - Object.freeze()有什么用?与const有什么区别?
Object.freeze适用于对象值,它能够让对象不可变(该对象属性不能修改)
let foo = {
a:'A'
}
let bar = {
b:'B'
}
Object.freeze(foo)
foo.a = 'a'
console.log(foo) // {a: "A"}
相比const,两者是不同的概念,const作用是声明变量(一个只读变量),一旦声明,这个变量就不能修改,而Object.freeze()作用是让一个对象不可变
55 - 细品new
new- 配合构造函数创建对象
function Person(name, age, job){
this.name = name
this.age = age
this.job = job
}
var person = new Person('CHICAGO', 21, 'itMan')
- 通过例子细品
new在创建对象的过程中做了哪4件事
function Person(){
this.name = 'CHICAGO'
}
new Person()
- 创建一个空对象 → var obj = {}
- 将空对象赋给this → this = obj
- 将空对象的 __proto__ 属性指向构造函数的 prototype → this.__proto__ = Person().prototype
- 返回这个对象(this)→ return this
- 总结
- 创建空对象
{} - 将空对象分配给
this值 - 将空对象的
__proto__指向构造函数的prototype - 如果没有使用显式
return语句,则返回this
- 创建空对象
56 - in 运算符和 Object.hasOwnProperty 方法有什么区别?
hasOwnProperty()- 返回一个布尔值,判断对象是否包含特定的自身(非继承)属性
判断自身属性是否存在
Object.prototype.c= 'C';
var obj = new Object()
obj.a = 'A'
function changeObj(){
obj.b = 'B'
delete obj.a
}
obj.hasOwnProperty('a') // true
obj.hasOwnProperty('c') // false
changeObj()
obj.hasOwnProperty('a') // false
obj.hasOwnProperty('b') // false
如果在函数原型上定义一个变量,hasOwnProperty()方法会直接忽略掉
in运算符
如果指定的属性在指定的对象或其原型链中,则in运算符返回true
Object.prototype.c= 'C';
var obj = new Object()
obj.a = 'A'
console.log('a' in obj) // true
console.log('c' in obj) // true
in运算符会检查它或者其原型链是否包含具有指定名称的属性
57 - 如何创建一个没有原型(prototype)的对象?
- 通过
Object.create()可以实现创建没有原型的对象
const objHavePrototype = {}
console.log(objHavePrototype.toString()) // [Object object]
const objHaveNoPrototype = Object.create(null)
console.log(objHaveNoPrototype.toString) // TypeError: objHaveNoPrototype.toString is not a function
typeof objHaveNoPrototype // object
我们知道 typeof null === 'object',但 null 并没有 prototype 属性
58 - 如何判断一个元素是否使用了event.preventDefault()
- 通过在事件对象中使用
event.defaultPrevented属性,该属性返回一个布尔值用于区分是否在特定元素中使用了event.preventDefault()
59 - 访问不存在的属性,为什么有时返回undefined,有时却是报错
var foo = {}
console.log(foo.a) // undefined
console.log(foo.a.A) // TypeError: Cannot read property 'A' of undefined
观察上面这个例子,有人会认为都是返回undefined或者都是报错,当我们访问foo.a的时候,由于foo对象并不存在a属性,所以返回的是undefined,而当我们去访问一个undefined的属性时,就会报出TypeError: Cannot read property 'XXX' of undefined的错误
60 - 为什么 0.1 + 0.2 != 0.3 ? 如何解决这个问题 ?
由于计算机是通过二进制来存储东西,那么0.1在二进制中会表示为
// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)
可以发现,0.1在二进制中是一个无限循环的数字,并不是精确的0.1,其实很多十进制小数用二进制表示都会是无限循环的,因为Javascript采用浮点数标准,导致会裁剪掉我们的数字,那么这些循环的数字被裁剪之后,就会出现精度丢失的问题,也就造成0.1不再是0.1,而是变成0.100000000000000002
0.100000000000000002 === 0.1 // true
自然,0.2在二进制中也是无限循环,所以
0.1 + 0.2 === 0.30000000000000004 // true
解决 0.1 + 0.2 != 0.3
parseFloat(str)- 解析一个字符串,并返回一个浮点数- 该函数指定字符串中的首个字符是否是数字。如果是,则对字符串进行解析,直到到达数字的末端为止,然后以数字返回该数字,而不是作为字符串
- str - 必需,要被解析的字符串
toFixed(num)- 把Number四舍五入为指定小数位数的数字- num - 必需,规定小数的位数(0 ~ 20)
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true
温馨提示😀
- 由于
Javascript内容较多,本章列举了较为重要的部分,个人会继续总结知识,对该章持续更新,后续会总结Js重点手写题,建议对本文进行收藏 - 下一期 - 总结
ES6核心知识点