重整旗鼓,2019自结前端面试小册【JavaScript】

前言

2020年已经到来,是否是该为了更好的2020年再战一回呢? ‘胜败兵家事不期,包羞忍耻是男儿。江东子弟多才俊,卷土重来未可知’,那些在秋招失利的人,难道就心甘情愿放弃吗!javascript

此文总结2019年以来本人经历以及浏览文章中,较热门的一些面试题,涵盖从CSS到JS再到Vue再到网络等前端基础到进阶的一些知识。css

总结面试题涉及的知识点是对本身的一个提高,也但愿能够帮助到同窗们,在2020年会有一个更好的竞争能力。前端

Module Two - JavaScript

1 - 基本类型

  • 简单数据类型(原始类型): String、Number、Boolean、Null、Undefined、Symbol
  • 复杂数据类型(对象类型): Object

Null是对象吗?java

虽然typeof null返回的是object,但这实际上是JavaScript长久以来遗留的一个bugnull实质上是基本数据类型的一种node

简单数据类型与复杂数据类型在数据存储上有什么区别?jquery

简单数据类型以的形式存储,存储的是es6

复杂数据类型以的形式存储,地址(指向堆中的值)存储在中。面试

❗ 小知识: 当咱们把对象赋给另一个变量时,复制的是地址,指向同一块内存空间,因此当其中一个对象改变时,另一个对象也会随之改变ajax

栈内存与对堆内存的区别

  • 栈内存
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 → 一、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 name = new Number('123')
console.log(typeof name.valueOf()) → number
console.log(typeof name.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 - 浅拷贝?深拷贝?如何实现?

  • 浅拷贝

一、首先能够经过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
复制代码

二、也能够经过展开运算符...来实现浅拷贝

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()
复制代码

这里,instanceObject实例Objectinstance构造函数,构造函数拥有一个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.prototypeObject.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 async>异步加载,但执行时会阻塞元素渲染

19 - null 与 undefined 有什么区别?

  • null 表示一个对象被定义了,但值是空值(定义为空)
  • undefined 表示不存在这个值
typeof null // 'Object'
null 是一个空对象,没有任何的属性和方法

typeof undefined // 'undefined'
undefined 是一个表示'无'的原始值或表示缺乏值,例如变量被声明了,但没有任何赋值时
复制代码

从内存来看null和undefined,本质区别是什么?

  • 给一个全局变量赋值为null,至关给这个属性分配了一块空的内存,而后值为null,Js会回收全局变量为null的对象
  • 给一个全局变量赋值为undefined,至关于将这个对象的值清空,可是这个对象依旧存在,若是是给对象的属性赋值为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中,咱们能够采用多种方式实现循环

  • while
  • do...while
  • for
  • for...in
  • for...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循环,breakcontinuereturn都不奏效。)
复制代码
  • 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中
          fn.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
- ···
复制代码

image

事件如何先冒泡后捕获?

对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件时,先暂停执行,直到冒泡事件被捕获后再执行事件


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.mapArray.prototype.filterArray.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, 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{
        ele.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有两种状态:一、等待(pending);二、完成(settled)
- promise会一直处于等待状态,直到它所包装的异步调用返回/超时/结束
- 这时候promise状态变成完成。完成状态分红两类:一、解决(resolved);二、拒绝(rejected)
- promise解决(resolved):意味着顺利结束。promise拒绝(rejected)意味着没有顺利结束
复制代码
  • Generator - Generator函数是一个状态机,封装了多个内部状态
  • async

49 - 同源策略

Javascript只能与同一个域中的页面进行通信

两个脚本被认为是同源的条件:

  • 协议相同
  • 端口相同
  • 域名相同

50 - jsonP的优缺点

  • 优势
    • 它不像XMLHttpRequest对象实现的AJAX请求那样受到同源策略的限制,jsonP能够实现跨越同源策略
    • 它的兼容性更好,在更加古老的浏览器中均可以运行,不须要XMLHttpRequestActiveX的支持
    • 在请求完毕后能够经过调用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响应已经彻底接收
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核心知识点
相关文章
相关标签/搜索