IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> Web 面试之 JavaScript -> 正文阅读

[JavaScript知识库]Web 面试之 JavaScript

Web面试之 JavaScript


前言

  • 面试会考察基础知识和原理
  • 看到题目时要了解该题考查的知识点
  • 拿到一个面试题,第一时间看到——考点

一、变量类型和计算

1、JS 数据类型有哪些?存储上的区别?

  • 基本数据类型:string、number、boolean、null、undefined、Symbol(ES6)、BigInt(ES10)
  • 引用类型:array、object、function、date 等

存储区别:

  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。
  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

2、值类型和引用类型区别?

  • 基本数据类型:声明并初始化一个变量 a,将 a 赋值给变量 b,然后改变 a 的值不影响 b 的值
  • 引用类型:声明并初始化一个引用类型的 m,将 m 赋值给同样引用类型的 n,m 和 n 指向同一个内存地址,如果改变 m 的值,n 的值也将发生改变

3、null 和 undefined 的区别?

  • Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null
  • undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化
  • 当我们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,当我们使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false

4、NaN 是什么的缩写?是什么意思?

  • NaN 是 Not a Number 的简写,是 JS 中的特殊值,表示非数字,NaN 不是数字,但是他的数据类型是数字,它不等于任何值,包括自身,在布尔运算时被当做 false,NaN 与任何数运算得到的结果都是NaN,党员算失败或者运算无法返回正确的数值的就会返回 NaN,一些数学函数的运算结果也会出现 NaN

5、typeof 能判断哪些类型?

  • 能识别所有值类型
let v;                 typeof v       // 'undefined'
const str = 'abc'      typeof str     // 'string'
const n = 100          typeof n       // 'number'
const b = true         typeof b       // 'boolean'
const s = Symbol('s')  typeof s       // 'symbol'
  • 识别函数
typeof console.log      // 'function'
typeof function() {}    // 'function'
  • 判断是否是引用类型
typeof null             // 'object'
// typeof undefined     // 'undefined'
typeof ['a', 'b']       // 'object'
typeof { x: 100 }       // 'object'

6、浅拷贝和深拷贝

  • 浅拷贝: 浅拷贝是指复制对象的时候,只对第一层键值对进行独立的复制,如果对象内还有对象,则只能复制嵌套对象的地址(浅拷贝:Object.assign()、slice()、concat()、扩展运算符)
  • 深拷贝: 深拷贝是指将原复制对象完全拷贝一份,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个(深拷贝:JSON.stringify(JSON.parse()))
  • 在深拷贝中,对象上的 value 值为 undefined 、 symbol和函数的键值对会被忽略;NaN、无穷大、无穷小会被转为 null
/**
 * 深拷贝
 * @Param { Object } obj 要考拷贝的对象
 **/
function deepClone(obj = {}) {
	if(typeof obj !== 'object' || obj == null) {
		// 如果 obj 不是 null,或者不是对象和数组,直接返回
		return obj;
	}
	// 根据obj的类型判断是新建一个数组还是对象
    let result = Array.isArray(obj)? [] : {};
	
	// 遍历
	for(let key in obj) {
		// 保证 key 不是原型的属性
		if(obj.hasOwnProperty(key)) {
			// 递归调用
			result[key] = deepClone(obj[key])
		}
	}
	return result
}

const obj1 = {
	name: 'zs',
	age: 12,
	addr: {
		city: 'beijing'
	},
	arr: ['a', 'b', 'c']
}

// 浅拷贝
const obj2 = obj1
obj2.addr.city = 'nanjing'
console.log(obj1.addr.city)     // 'nanjing'

// 深拷贝
const obj3 = deepClone(obj1)
obj3.addr.city = 'nanjing'
obj3.arr[0] = 'aa'
console.log(obj1.addr.city)     // 'beijing'
console.log(obj1.arr[0])        // 'a'

7、字符串拼接

  • 只要与字符串进行拼接,就会得到字符串
const a = 100 + 10        // 110
const b = 100 + '10'      // '10010'
const c = true + '10'     // 'true10'

8、== 运算符

  • 使用 “==” 运算符进行比较,“==” 会尝试着尽可能让左右两边相等
100 == '100'          // true
0 == ''               // true
0 == false            // true
false == ''           // true
null == undefined      // true

9、何时使用 “===”,何时使用 “==”?

  • “==” 在JavaScript 中会进行隐式类型转换,在等号两边进行比较时会先进行类型转换,再确定操作数是否相等
  • 全等操作符由 3 个等于号( === )表示,只有两个操作数在不转换的前提下相等才返回 true,即类型相同,值也需相同
  • 一般除了判断是否等于 null 时使用两等“==”之外,其它都一律使用三等“ ===”
const obj = { a: 100 }
if(obj.a == null) {}
// 相当于
if(obj.a === null || obj.a === undefined) {}

10、if 语句和逻辑运算

  • truly 变量:!!a === true 的变量
  • falsely 变量:!!a === false 的变量
// 以下是 falsely 变量。除此之外都是 truely 变量
!!0 === false
!!NaN === false
!!'' === false
!!null === false
!!undefined === false
!!false === false

11、数组常用的方法?

push() 方法:

  • push() 方法用来 在数组末尾推入新项,参数就是要推入的项
  • 如果要推入多项,可以用逗号隔开,推入的项直接放到数组尾部,相当于在原数组后补充一个数组
  • 调用 push() 方法后,数组会立即改变,不需要赋值
var arr = [1,2,3];
arr.push(11);
arr.push(21,22,23);
console.log(arr);     // [1, 2, 3, 11, 21, 22, 23]

pop() 方法:

  • 与 push() 方法相反,pop() 方法用来 删除数组中的最后一项
  • pop() 方法不仅会删除数组末项,而且还会返回被删除的项
  • 如果在 pop() 方法的圆括号内添加参数,参数没有任何意义,也不会报错
var arr = [11, 22, 33, 44, 55];
var item = arr.pop();
console.log(item);          // 55
console.log(arr);           // [11, 22, 33, 44]

unshift() 方法:

  • unshift() 方法用来 在数组头部插入新项,参数就是要插入的项
  • 如果要推入多项,可以用逗号隔开,推入的项直接放到数组头部,相当于在原数组前补充一个数组
  • 调用 unshift() 方法后,数组会立即改变,不需要赋值
var arr = [1,2,3];
arr.unshift(11);
arr.unshift(21,22,23);
console.log(arr);     // [21, 22, 23, 11, 1, 2, 3]

shift() 方法:

  • 与 unshift() 方法相反,shift() 方法用来 删除数组中下标为 0 的项
  • shift() 方法不仅会删除数组末项,而且还会返回被删除的项
  • 如果在 shift() 方法的圆括号内添加参数,参数没有任何意义,也不会报错
var arr = [11, 22, 33, 44, 55];
var item = arr.shift();
console.log(item);          // 11
console.log(arr);           // [22, 33, 44, 55]

splice() 方法:

  • splice(start, length, [params …]) 方法用于 替换数组中的指定项
  • start 表示起始项,从下标为 start 的项开始替换
  • length 表示替换多少项
  • [params …] 表示替换成的内容
  • splice() 方法会以数组形式返回被删除的项
var arr = ['a','b','c','d','e','f'];
// 1.表示从下标为3项开始,将后面的 2 项替换为 1,2,3
arr.splice(3,2,1,2,3);
console.log(arr);     // ['a','b','c', 1, 2, 3,'f']
// 2.表示在数组下标为 3 的位置插入 1,2,3
arr.splice(3,0,1,2,3);
console.log(arr);     // ['a','b','c', 1, 2, 3, ‘d', 'e', 'f']
// 3.表示从数组下标为 3 的位置开始删除 2 项
arr.splice(3,2);
console.log(arr);     // ['a','b','c', 'f']
// 4.表示从数组下标为 3 的位置开始删除后面所有项
var items = arr.splice(3);
console.log(arr);     // ['a','b','c']
console.log(items);     // ['d','e','f']

slice() 方法:

  • slice() 方法用于得到 子数组,类似于字符串的 slice() 方法
  • slice(a, b) 截取的子数组 从下标为 a 的项开始,到下标为 b 的项结束(不包括下标为 b 的项)
  • slice() 方法不会更改原数组
  • slice() 如果不提供第二个参数,则表示从指定项开始,提取所有后续项作为子数组
  • slice() 方法的 参数允许为负数,表示数组的倒数第几项
var arr = ['a','b','c','d','e','f'];

var child_arr1 = arr.slice(3,5);
var child_arr2 = arr.slice(3);
var child_arr3 = arr.slice(3,-2);
var child_arr4 = arr.slice(-4, -2);

console.log(child_arr1);        // ['d','e']
console.log(child_arr2);        // ['d','e','f']
console.log(child_arr3);        // ['d']
console.log(child_arr4);        // ['c', 'd']
console.log(arr);               // ['a','b','c','d','e','f']

join() 和 split() 方法:

  • 数组的 join() 方法可以 使数组转为字符串;字符串的 split() 方法可以使字符串转为数组
  • join() 的参数表示 以什么字符作为连接符,如果 留空则默认以逗号分隔,如同 toString() 方法
  • split() 的参数表示以什么字符拆分字符串,一般不能留空
var arr = ['a','b','c','d'];
// 不写参数,默认使用“,”分隔
var arr_str = arr.join();
console.log(arr_str);                  // a,b,c,d
// 使用“-”作为分隔符
var arr_str1 = arr.join('-');
console.log(arr_str1);                 // a-b-c-d
// 使用空字符串作为分隔符
var arr_str2 = arr.join('');
console.log(arr_str2);                 // abcd
// 不写参数,会将整个字符串当做一个数组项
var str_arr = arr_str.split();
console.log(str_arr);                  // ["a,b,c,d"]
// 使用空字符串,会连带将分隔符一起作为数组项
var str_arr = arr_str.split('');
console.log(str_arr);                  // ["a", ",", "b", ",", "c", ",", "d"]
// 使用分隔符分隔,会将每一个字符作为一项
var str_arr = arr_str.split(',');
console.log(str_arr);                  // ["a", "b", "c", "d"]

字符串和数组更多相关特性:

  • 字符串也可以使用方括号内写下标的形式访问某个字符,等价于 charAt() 方法
  • 这样的话,就不常使用 charAt() 了
  • 字符串的一些算法问题有时候会转换为数组解决
var str = "hello world!";
console.log(str[0]);        // "h"
console.log(str[2]);        // "l"
console.log(str[5]);        // " "
console.log(str[11]);       // "!"
console.log(charAt(0));        // "h"
console.log(charAt(2));        // "l"
console.log(charAt(5));        // " "
console.log(charAt(11));       // "!"

concat() 方法:

  • concat() 方法可以 合并连结多个数组
  • concat() 方法并不会改变原数组
var arr1 = [1,2,3];
var arr2 = [4,5,6];
var arr3 = [7,8,9,10,11];
var arr = arr1.concat(arr2, arr3);
console.log(arr);   // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
console.log(arr1);  // [1, 2, 3]

sort() 方法:

  • sort 对数组元素进行排序(改变原数组)
  • 数组在原数组上进行排序,不生成副本

reverse() 方法:

  • reverse() 方法用来将一个数组中的全部项顺序置反
var arr = ['A','B','C','D','E','F','G'];
arr.reverse();
console.log(arr);    // ["G", "F", "E", "D", "C", "B", "A"]
// 将一个字符串置反
var str = "abcdefg";
console.log(str.split('').reverse().join(''));  // "gfedcba"
// 逐步拆解如下:
var str_arr = str.split('');
console.log(arr);  // ["a", "b", "c", "d", "e", "f", "g"]
arr.reverse();
var arr_str = arr.join('');
console.log(arr_str);  // "gfedcba"

indexOf 和 includes() 方法:

  • indexOf() 方法的功能是 搜索数组中的元素,并返回它所在的位置,如果元素不存在,则返回 -1
var arr = ['a','b','c','d','e','c'];
// 搜索数组中的元素,并返回它所在的位置(下标数)
var index1 = arr.indexOf('a');
console.log(index1);     // 0
// 当数组中多次出现某个元素,则返回第一次出现时所在的下标数
var index2 = arr.indexOf('c');
console.log(index2);     // 2
// 当搜索的元素不存在,则返回 -1
var index3 = arr.indexOf('g');
console.log(index3);     // -1
  • includes() 方法的功能是 判断一个数组是否包含一个指定的值,返回布尔值
var arr = ['a','b','c','d','e','c'];
// 数组是否包含一个指定的值,返回布尔值
var flag1 = arr.includes('a');
console.log(flag1);     // true
var flag2 = arr.includes('g');
console.log(flag2);     // false
  • indexOf 和 includes() 方法在判断项的位置或项的值时使用的是 === 进行比较(既比较类型也比较值)
var arr = ['a','b','c',11,22];
var flag1 = arr.includes('a');
console.log(flag1);     // true
var flag2 = arr.includes(22);
console.log(flag2);     // true
var flag3 = arr.includes('11');
console.log(flag3);     // false
var flag4 = arr.includes(b);
console.log(flag4);     // 报错
var index1 = arr.indexOf('b');
console.log(index1);     // 1
var index2 = arr.indexOf(22);
console.log(index2);     // 4
var index3 = arr.indexOf('11');
console.log(index3);     // -1

find() 方法:

  • find() 方法主要用于查找 第一个 符合条件的数组元素,只要查找到就立即停止继续查找
  • 它的参数是一个回调函数,为数组中的每个元素都调用一次函数执行,在回调函数中可以写你要查找元素的条件,当条件成立为 true 时,返回该元素,之后的值不会再调用执行函数;如果没有符合条件的元素,返回值为undefined
  • 它的回调函数有三个参数:value:当前的数组元素;index:当前索引值;arr:被查找的数组
var arr = [1,2,3,4,5]
var value = arr.find((val) => { 
	return val > 2 
})
console.log(value)    // 3

arr.find((value,index,array) => {
	
})

filter() 方法:

  • filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
  • 它的参数是一个回调函数,有三个参数:value:当前的数组元素;index:当前索引值;arr:被查找的数组

toString() 方法:

  • toString() 方法将数组转为字符串

12、字符串常用方法?

  • 增: +、${}、concat
  • 删: slice()、substr()、substring()
  • 改:trim()、trimLeft()、trimRight()、repeat()
  • 查:chatAt()、indexOf()、includes()、startWith()
  • 转换:toLowerCase()、toUpperCase()、split()
  • 迭代:some()、every()、forEach()、filter()、map()

13、数组去重

  • 利用 for 循环的嵌套,然后splice去重(ES5中最常用)
var arr = [1,1,2,3,2,4,5,5,6];
function unique(arr) {
    for (var i = 0; i < arr.length; i++) {    // 首次遍历数组
        for (var j = i + 1; j < arr.length; j++) {   // 再次遍历数组
            if (arr[i] == arr[j]) {          // 判断连个值是否相等
                arr.splice(j, 1);           // 相等删除后者
                j--;
            }
        }
    }
    return arr
}
console.log(unique(arr));
  • 利用 indexOf() 去重
var arr = [1,1,2,3,2,4,5,5,6];
function unique(arr) {
    var array = [];
    for (var i = 0; i < arr.length; i++) {
        if (array.indexOf(arr[i]) === -1) {   // 判断索引有没有等于
            array.push(arr[i])
        }
    }
    return array
}
console.log(unique(arr));
  • ES6 中 Set 去重(ES6中最常用)
var arr = [1,1,2,3,2,4,5,5,6];
function unique (arr) {
  return Array.from(new Set(arr))
}
console.log(unique(arr))
  • 利用 includes()
var arr = [1,1,2,3,2,4,5,5,6];
function unique (arr) {
	var result = [];
	for(var i = 0; i < arr.length; i++) {
		if(!result.includes(arr[i])) {
			result.push(arr[i]);
		}
	}
	return result
}
console.log(unique (arr));  // [1, 2, 3, 4, 5, 6]

二、原型和原型链

1、JS 中继承实现的几种方式?

1、ES5 如何实现继承?

  • 原型链继承: 原型链继承直接让子类的原型指向父类的实例,当子类实例在自身找不到对应的属性和方法时,就会向它的原型对象,也就是父类实例上找,从而实现对父类的属性和方法的继承
    缺点: 由于所有 Child 实例原型都指向同一个 Parent 实例,因此对某个 Child 实例的父类引用类型变量修改会影响所有的 Child 实例
	// 父类
    function Parent() {
      this.name = 'hello'
    }
    // 父类的原型方法
    Parent.prototype.getName = function () {
      return this.name
    }
    // 子类
    function Child() {}
 
    // 让子类的原型对象指向父类实例, 这样一来在Child实例中找不到的属性和方法就会到原型对象(父类实例)上寻找
    Child.prototype = new Parent()
  • 构造函数继承: 即在子类的构造函数中执行父类的构造函数,并为其绑定子类的 this,让父类的构造函数把成员属性和方法都挂到子类的 this上去,这样既能避免实例之间共享一个原型实例,又能向父类构造方法传参
    缺点: 找不到父类原型上的属性和方法
	function Parent(name) {
      this.name = [name]
    }
    Parent.prototype.getName = function () {
      return this.name
    }
 
    function Child() {
      // 执行父类构造方法并绑定子类的this, 使得父类中的属性能够赋到子类的this上
      Parent.call(this, 'test') 
    }
  • 实例继承:为父类实例添加新特性,作为子类实例返回。实例继承的优点是不限制调用方法,不管是new 子类()还是子类()返回的对象具有相同的效果;缺点是实例是父类的实例,不是子类的实例,不支持多继承
  • 拷贝继承:优点:支持多继承;缺点:效率较低,内存占用高(因为要拷贝父类的属性)无法获取父类不可枚举的方法(不可枚举方法,不能使用 for in 访问到)
  • 组合继承:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
  • 寄生组合继承:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

2、ES6 如何用 Class 实现继承

  • 子类使用 extends 继承父类,在子类的构造函数中使用 super 继承父类的属性,在子类可以重写父类的方法
class People {
	constructor(name) {
		this.name = name
	}
	eat() {
		console.log(`${this.name} eat something`)
	}
}

// 子类
class Student extends People {
	constructor(name, number) {
		super(name)
		this.number = number
	}
	sayHi() {
		console.log(`姓名:${this.name} 学号:${this.number}`)
	}
}

// 创建实例
const zs = new Student('张三', 16030222)
console.log(zs.name)
console.log(zs.number)
zs.eat()
zs.sayHi()

3、instanceof 判断类型

  • instanceof 用于判断一个变量是否是某个对象的实例
zs instanceof Student        // true
zs instanceof People         // true
zs instanceof Object        // true

Student instanceof People    // false
Student instanceof Object    // true

[] instanceof Array          // true
[] instanceof Object         // true

{} instanceof Object         // true

4、理解原型和原型链

  • 原型:在 JS 中我们使用构造函数来新建一个对象时,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象包含了所有该构造函数实例共享的属性和方法。当我们使用构造函数新建一个实例后,在这个实例的内部将包含一个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。
  • 原型链:在 JavaScript 中,所有的对象都拥有一个__proto__ 属性指向该对象的原型(prototype),这个原型对象又会有自己的__proto__ 属性指向它的原型,这样一直下去,最终直到 Object.prototype.proto = null,这个过程就形成了原型链
  • 隐式原型:__proto__
  • 显示原型:prototype
  • 每个 Class 都有显示原型 prototype
  • 每个实例都有隐式原型 __proto__
  • 实例的 __proto__ 指向对应 Class 的 prototype
  • 获取属性或执行方法时,先在自身属性和方法寻找,如果找不到则自动去 __proto__ 中查找
// Class 实际上是函数
typeof People       // 'function'
typeof Student      // 'function'

// 隐式原型和显示原型
console.log(zs.__proto__)
console.log(Student.prototype)
console.log(zs.__proto__ === Student.prototype)

5、hasOwnProperty

  • hasOwnProperty表示是否有自己的属性。这个方法会查找一个对象是否有某个属性,但是不会去查找它的原型链

6、new操作符干了什么?

  • 创建一个新的对象obj
  • 将对象与构造函数通过原型链连接起来
  • 将构造函数中的this绑定到新建的对象obj上
  • 根据构造函数返回类型作判断,如果构造方法返回了一个对象,那么返回该对象,否则返回第一步创建的新对象
    function myNew(Func, ...args) {
      // 1.创建一个新对象
      const obj = {}
      // 2.新对象原型指向构造函数原型对象
      obj.__proto__ = Func.prototype
      // 3.将构建函数的this指向新对象
      let result = Func.apply(obj, args)
      // 4.根据返回值判断
      return result instanceof object ? result : obj
    }
 
    //测试
    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    Person.prototype.say = function () {
      console.log(this.name)
    }
    let p = myNew(Person, "test", 123)
    console.log(p) // Person {name: "test", age: 123}

7、typeof 和 instanceof 的区别

  • typeof 操作符返回一个操作数的类型
  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

typeof与instanceof都是判断数据类型的方法,区别如下:

  • typeof会返回一个变量的基本类型,instanceof 返回的是一个布尔值
  • instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型
  • 而typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断

8、如何准确判断一个变量是不是数组?

  • a instanceof Array

9、class 的原型本质,怎么理解?

  • 通过原型和原型链向上一步一步的找对应属性和方法

三、作用域和闭包

1、作用域和自由变量

作用域:

  • 作用域:即变量生效的区域
  • 全局作用域:如 window、document 等这种不在函数或是大括号中声明,却可以在程序的任意位置访问的变量
  • 函数作用域:函数作用域也叫局部作用域,在函数中声明,且只能在当前函数内部访问
  • 块级作用域(ES6新增):在 if、for、while 等语句块里定义的变量

作用域链(自由变量):

  • 在 Javascript 中,如果一个变量在当前作用域没有定义,但被使用了,首先Javascript 引擎会尝试向上级作用域一层一层依次寻找,直至找到为止,如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量或是直接报错 xx is not defined

2、说说你对作用域链的理解

作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的。

3、什么是闭包?闭包的作用是什么?

什么是闭包:

  • 闭包就是能够读取其他函数内部变量的函数。

闭包的作用:

  • 使用闭包主要是为了设计私有的方法和变量,闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。
function createCache() {
	const data = {}
	return {
		set: function(key, val) {
			data[key] = val
		},
		get: function(key) {
			return data[key]
		}
	}
}

闭包作用域应用的特殊情况,有两种表现:

  • 函数作为参数被传递
function print(fn) {
  let a = 202
  fn()
}
let a = 102
function fn() {
  console.log(a)
}
print(fn) // 102
  • 函数作为返回值被返回
function create() {
  let a = 101
  return function () {
    console.log(a)
  }
}

const fn = create()
let a = 201
fn()

4、this对象的理解?

  • 每一个函数都会有一个this对象,this对象是函数在执行时的那个环境,也可以说是这个函数在那个作用域下运行的
  • this 在各个场景中取什么样的值是在函数执行的时候确定的,不是在定义的时候被确定,this 永远指向最后调用它的那个对象
  • 箭头函数在定义的时候就确定 this,它的 this 指向在定义的时候继承自外层第一个普通函数的 this

this 的应用场景(调用模式,如何取值):

  • 函数调用模式:作为普通函数去调用,this 指向全局对象 window
  • 方法调用模式:作为对象中的方法被调用,this 指向这个对象本身
  • 构造器调用模式:如果一个函数用 new 调用时,函数执行前会新创建一个对象实例,this 指向这个新创建的对象实例
  • call 、apply 、bind 方法调用模式:使用 call()、apply()、bind() 方法调用时,this 指向指定传入的对象

this 如何取值:

  • this 在各个场景中取什么样的值是在函数执行的时候确定的,不是在定义的时候被确定,this 永远指向最后调用它的那个对象
  • 对象打点调用它的方法函数,则函数的上下文是这个打点的对象
  • 圆括号直接调用函数,则函数的上下文是window 对象
  • 数组(类数组对象)枚举出函数进行调用,上下文是这个数组(类数组对象)
  • IIFE中的函数,上下文是 window 对象
  • 使用定时器、延时器调用函数,上下文是 window 对象
  • 事件处理函数的上下文是绑定事件的 DOM 元素

5、call、apply、bind 三者之间的区别?

  • call、apply、bind 作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向
  • call 方法的第一个参数是 this 的指向,后面传入的是一个参数列表(参数要用逗号分隔罗列出来);改变 this 指向后原函数会立即执行,且此方法只是临时改变 this 指向一次
  • apply 接受两个参数,第一个参数是 this 的指向,第二个参数是函数接受的参数,参数必须是以数组或类数组形式传入,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次
  • bind 方法和 call 方法很相似,第一参数也是 this 的指向,后面传入的也是一个参数列表,但改变 this 指向后不会立即执行,而是返回一个永久改变 this 指向的函数,等到真正触发的时候才执行,bind不兼容IE6~8

6、手写 call、apply、bind 函数

  • call 和 apply的原理都是给那个对象加个属性,调用拿到返回值后再delete掉
  • bind 的原理是返回一个函数,这个函数最终使用的参数是创建和执行的参数的集合
// 手写 call
Function.prototype.myCall = function (context, ...args) {
	// 声明一个独有的Symbol属性, 防止fn覆盖已有属性
    const fn = Symbol() 
    
    // 若没有传入this, 默认绑定window对象
    if(!context) context = window 
    
    // this指向调用call的对象,即我们要改变this指向的函数
    context[fn] = this 
    
    // 执行当前函数
    const result = context[fn](...args) 
    // 删除我们声明的fn属性
    delete context[fn]
    // 返回函数执行结果
    return result 
}

// 手写 apply
Function.prototype.myApply = function (context, args =[]) {
	// 声明一个独有的Symbol属性, 防止fn覆盖已有属性
    const fn = Symbol() 
    
    // 若没有传入this, 默认绑定window对象
    if(!context) context = window 
    
    // this指向调用call的对象,即我们要改变this指向的函数
    context[fn] = this 
    
    // 执行当前函数
    const result = context[fn](...args) 
    // 删除我们声明的fn属性
    delete context[fn]
    // 返回函数执行结果
    return result 
}

// 手写 bind
Function.prototype.myBind = function (context, ...args) {
  const self = this;
  if(!context) context = window;
  return function (...otherArgs) {
    return self.apply(context, [...args, ...otherArgs])
  }
}

Function.prototype.myBind = function(context, ...args) {
	// 将参数拆解为数组
	const args = Array.prototype.slice.call(arguments)

	// 获取 this (数组的第一项)
	const th = args.shift()
	
	// 获取 fn.bind(…) 中的 fn
	const self = this
	
	// 返回一个函数
	return function() {
		return self.apply(th, args)
	}
}

7、说说var、let、const 之间的区别?

变量提升:

  • var 声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined
  • let 和 const 不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错

暂时性死区:

  • var 不存在暂时性死区
  • let 和 const 存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

块级作用域:

  • var 不存在块级作用域
  • let 和 const 存在块级作用域

重复声明:

  • var 允许重复声明变量
  • let 和 const 在同一作用域不允许重复声明变量

修改声明的变量:

  • var 和 let 可以
  • const声明一个只读的常量,一旦声明,常量的值就不能改变

使用:

  • 能用 const 的情况尽量使用 const,其他情况下大多数使用let,避免使用var

8、JS 创建 10 个 <a> 标签,点击的时候弹出对应的序号

let a
for(let i = 0; i < 10; i++) {
	a = document.createElement('a');
	a.innerHTML = i + '<br>'
	a.addEventListener('click', function(e) {
		e.preventDefault()
		alert(i)
	})
	document.body.appendChild(a)
}

9、普通函数和箭头函数的区别?

  • 语法更加简洁、清晰
  • 箭头函数没有 prototype (原型),所以箭头函数本身没有this
    箭头函数没有自己的this,箭头函数的this指向在定义(注意:是定义时,不是调用时)的时候继承自外层第一个普通函数的this。所以,箭头函数中 this 的指向在它被定义的时候就已经确定了,之后永远不会改变。
  • call | apply | bind 无法改变箭头函数中this的指向
    call | apply | bind方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向。
  • 箭头函数不能作为构造函数使用
    我们先了解一下构造函数的new都做了些什么?简单来说,分为四步: ① JS内部首先会先生成一个对象; ② 再把函数中的this指向该对象; ③ 然后执行构造函数中的语句; ④ 最终返回该对象实例。
    但是!!因为箭头函数没有自己的this,它的this其实是继承了外层执行环境中的this,且this指向永远不会随在哪里调用、被谁调用而改变,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new调用时会报错!
  • 箭头函数不绑定arguments,取而代之用rest参数…代替arguments对象,来访问箭头函数的参数列表
    箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值

四、异步

单线程和异步:

  • JS 是单线程语言,同一时刻只能做一件事
  • 浏览器和 nodejs 已支持 JS 启动进程,如 Web Worker
  • JS 和 DOM 渲染共同使用一个线程,因为 JS 可修改 DOM 结构

为什么需要异步:

  • 遇到等待(网络请求,定时任务)不能卡住
  • 所有就需要异步解决单线程等待的问题
  • 异步是基于 callback 函数形式调用

1、异步和同步的区别是什么?

  • 简单来说,同步异步的区别是:同步会阻塞代码执行;异步不会阻塞代码执行
  • 同步任务:指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
  • 异步任务:指的是对于不进入主线程、而进入"任务队列"(task queue)的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。

2、异步在前端的使用场景?

  • 网络请求,如 Ajax 图片加载
  • 定时任务,如 setTimeout

3、说说你对 Promise 的理解?

  • Promise 是一种解决异步编程的方案,常用于解决回调地狱(callback hell)层层嵌套的问题,将层层嵌套的回调函数转变为一个个的 .then 串联
  • 在 ES6 时将 Promise 写进了语言标准,统一了用法,原生提供了Promise 对象,并且 ES6 规定,Promise 对象是一个构造函数,用来生成 Promise实例
  • 依照 Promise/A+ 的定义,Promise 有3种状态:pending:初始状态fulfilled: 成功的操作rejected:失败的操作
  • Promise 的状态一旦改变,就不会再变,任何时候都可以得到这个结果,状态不可以逆,只能由 pending 变成 fulfilled 或者由 pending 变成 rejected

Promise 的缺点:

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消
  • 如果不设置回调函数,Promise内部抛出的错误,不会反映到外部
  • 当处于pending状态时,无法得知目前进展到哪一个阶段,是刚刚开始还是即将完成

Promise 的基本用法如下:

  • Promise构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和reject,它们是两个函数,由 JavaScript 引擎提供
  • resolve 函数的作用是将 Promise 对象的状态从 pending 变为 resolved,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
  • reject 函数的作用是,将Promise对象的状态从 pending 变为 rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
  • Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数,但日常操作中,更多的是使用 then 方法作为成功的回调,catch 方法作为失败时的回调

4、Promise 的简单实现原理

  • 首先,Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject
  • 然后写 then 方法,then方法接收两个函数 onFulfilled onRejected,状态是成功态的时候调用 onFulfilled 传入成功后的值,失败态的时候执行 onRejected,传入失败的原因,pending 状态时将成功和失败后的这两个方法缓存到对应的数组中,当成功或失败后 依次再执行调用
  • catch 方法是.then(null,onrejected) 的别名,用于指定发生错误时的回调函数,作用和 then 中的 onrejected 一样,不过它还可以捕获 onfulfilled 抛出的错,这是 onrejected 所无法做到的(Promise错误具有"冒泡"的性质,如果不被捕获会一直往外抛,直到被捕获为止;而无法捕获在他们后面的Promise抛出的错)
  • finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。finally方法不接受任何参数,故可知它跟Promise的状态无关,不依赖于Promise的执行结果
// 首先,Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject
function Promise(Fun) {
	// 缓存this
	let that = this
	// 设置此时的状态
    that.status = 'pending'
    // 设置初始值
    that.value = undefined
    
    // 用于存放成功后要执行的回调函数的序列
    that.onResolvedCallbacks = []
    // 用于存放失败后要执行的回调函数的序列
    that.RejectedCallbacks = []
    
    // 该方法是将 Promise 由 pending 状态变成 fulfilled(resolved)状态
    function resolve (value) {
        if (that.status == 'pending') {
            that.status = 'fulfilled';
            that.value = value;
            that.onResolvedCallbacks.forEach(cb => cd(that.value))
        }
    }
    // 该方法是将 Promise 由 pending 状态变成 rejected 状态
    function reject (reason) {
        if (that.status == 'pending') {
            that.status = 'rejected';
            that.value = reason;
            that.onRjectedCallbacks.forEach(cb => cd(that.value))
        }
    }

    try {
        // 每一个 Promise 在 new 一个实例的时候接受的函数都是立即执行的
        Fun(resolve, reject)
    } catch (e) {
        reject(e)
    }
}

// 第二部 写then方法,接收两个函数onFulfilled onRejected,状态是成功态的时候调用onFulfilled 传入成功后的值,失败态的时候执行onRejected,传入失败的原因,pending 状态时将成功和失败后的这两个方法缓存到对应的数组中,当成功或失败后 依次再执行调用
Promise.prototype.then = function(onFulfilled, onRejected) {
    let that = this;
    if (that.status == 'fulfilled') {
        onFulfilled(that.value);
    }
    if (that.status == 'rejected') {
        onRejected(that.value);
    }
    if (that.status == 'pending') {
        that.onResolvedCallbacks.push(onFulfilled);
        that.onRjectedCallbacks.push(onRejected);
    }
}

//catch原理就是只传失败的回调
Promise.prototype.catch = function(onRejected){
    this.then(null,onRejected);
}

5、手写用 Promise 加载一张图片

function loadImg(src) {
	const p = new Promise((resolve, reject) => {
		const img = document.createElement('img')
		img.onload = () => {
			resolve(img)
		}
		img.onerror = () => {
			const err = new Error(`图片加载失败 ${src}`)
			reject(err)
		}
		img.src = src
	})
	return p
}

const url = ''

loadImg(url).then(img => {
	console.log(img.width)
	console.log(img.height)
}).catch(err => {
	console.log(err)
})

const url1 = ''
const url2 = ''

loadImg(url1).then(img1 => {
	console.log(img1.width)
	console.log(img1.height)
	return loadImg(url2)
}).then(img2 => {
	console.log(img.width)
	console.log(img.height)
}).catch(err => {
	console.log(err)
})

6、Promise.all() 方法

  • Promise 的 all 方法接受一个数组作为参数,但数组内每个数必须是一个 Promise 实例
  • Promise 的 all 方法提供了并行执行异步操作的能力,并且在所有异步操作都执行完毕后才执行回调,返回一个Promise实例,这个Promise 实例的状态取决于参数的 Promise 实例的状态变化,只有当所有的实例都处于resolved 状态时,返回的Promise实例会变为resolved 状态;如果参数中任意一个实例处于 rejected 状态,那么返回的 Promise 即为 rejected 状态,此时第一个被 reject 的实例的返回值,会传递给Promise.all 的回调函数

7、Promise.race() 方法

  • Promise 的 race方法和 all 方法类似,也接受一个数组作为参数,数组内都是Promise 实例
  • Promise 的 race 方法返回的状态取决于第一个返回的实例的状态,不管结果本身是成功状态还是失败状态(顾名思义,race就是赛跑的意思,意思就是说Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那哪个结果

8、promise.all()和 promise.race的区别?

  • promise.all 是数组里面所有的 promise对象执行结束之后会返回一个存储所有 promise对象的结果
  • promise.race 顾名思义 race就是比赛的意思 只会返回一个执行速度最快的那个promise对象返回的结果,Promise.race()其他的异步函数照样还是会执行的,只是不会再执行resolve和reject,也不会返回结果了,但函数还是会执行的
    let runA = new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log('runA')
        resolve('a')
      }, 3000)
    })
 
    let runB = new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log('runB')
        resolve('b');
      }, 4000)
    })
// Promise.all([runA, runB]).then((res) => { console.log(res) }); 
//输出了["a", "b"],打印了runA和runB
// Promise.race([runA, runB]).then((res) => { console.log(res) }); 
//输出了a,打印了runA和runB
// Promise.race()其他的异步函数照样还是会执行的,只是不会再执行resolve和reject,也不会返回结果了,但函数还是会执行的
//上面代码还是会打印console.log('runB') 只会不会执行下面的resolve('b') 

9、promise.all 中,其中一个promise 出错,如何确保执行到最后?

在promise.all 队列中,使用 map 过滤每一个 promise 任务,其中任意一个报错后,return 一个返回值,p.catch 方法返回值会被 promise.reslove() 包裹,这样传进 promise.all 的数据全都是 resolved 状态的,确保 promise 能正常执行走到.then中

    let p1 = new Promise((resolve, reject) => {
      resolve({
        code: 200,
        list: []
      });
    });
 
    let p2 = new Promise((resolve, reject) => {
      resoLve({
        code: 200,
        list: []
      });
    });
 
    let p3 = new Promise((resolve, reject) => {
      reject({
        code: 500,
        msg: "服务异常"
      });
    });
    
    Promise.all([p1, p2, p3].map(p => p.catch(e => e)))
      .then(res => {
        console.log(res);
        /*
        打印结果:
        {code: 200, list: Array(0)}
        {code: 200, list: Array(0)}
        {code: 500, msg:”服务异常”}
        */
      }).catch(err => {
        console.log(err);
      })

10、图片懒加载实现原理?

  • 一张图片就是一个<img>标签,浏览器是否发起请求图片是根据<img>的 src 属性,所以实现懒加载的关键就是,在图片没有进入可视区域时,先不给<img>的src 赋值,这样浏览器就不会发送请求了,等到图片进入可视区域再给 src 赋值
  • 当图片距离顶部的距离 top-height 等于可视区域 h 和滚动区域高度 s 之和时说明图片马上就要进入可视区了,就是说当 top-height <= s+h 时,图片在可视区

11、await后面跟的对象

  • await 后面可以跟任何js表达式都不会报错,但是,只有当后面跟的是promise对象时,才会解决异步问题,实现同步
  • 后面跟普通js表达式
    对于非promise对象,比如箭头函数,同步表达式等等,会立即执行,而不是等待其执行结果
    function waitme() {
      setTimeout(() => {
        console.log(111111);
      }, 3000)
    }
    async function go() {
      await waitme();
      console.log(222222);
    }
    go(); // 输出结果为22222  111111
  • 后面跟 promise 对象
    对于promise对象,await会阻塞函数执行,等待promise的resolve返回值,作为await的结果,然后再执行下下一个表达式
    function waitme() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log(111111);
          resolve()
        }, 3000)
      })
    }
    async function go() {
      await waitme();
      console.log(222222);
    }
    go() //输出结果为111111 22222

12、异步的发展历程?

回调函数(callback):

  • 优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)
  • 缺点:回调地狱,不能用 try catch 捕获错误,不能 return。缺乏顺序性,回调地狱导致的调试困难,和大脑的思维方式不符。嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)嵌套函数过多的多话,很难处理错误
    ajax('xxx1', () => {
      // callback函数体
      ajax('xxx2', () => {
        // callback 函数体
        ajax('xxx3', () => {
          // callback函数体
        })
      })
    })

Promise:

  • Promise就是为了解决callback的问题而产生的。Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装
  • 优点:解决了回调地狱的问题
  • 缺点:无法取消 Promise ,错误需要通过回调函数来捕获
    let promise = new Promise((resolve, reject) => {
      //在这里执行异步操作
      if ( /*异步操作成功*/ ) {
        resolve(success)
      } else {
        reject(error)
      }
    })

Generator:

  • 特点:可以控制函数的执行,可以配合 co 函数库使用
  • 优点:函数体内外的数据交换、错误处理机制
  • 缺点:流程管理不方便
    function* fetch() {
      yield ajax('xxx1', () => {})
      yield ajax('xxx2', () => {})
      yield ajax('xxx3', () => {})
    }
    let it = fetch()
    let result1 = it.next()
    let result2 = it.next()
    let result3 = it.next()

async/await:

  • async、await 是异步的终极解决方案
  • 优点是:代码清晰,不用像Promise写一大堆 then 链,处理了回调地狱的问题
  • 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。
    async function test() {
      //以下代码没有依赖性的话,完全可以使用Promise.all的方式
      //如果有依赖性的话,其实就是解决回调地狱的例子了
      await fetch('XXX1')
      await fetch('XXX2 ')
      await fetch('XXX3')
    }

五、DOM 操作

1、DOM的常见操作 API

  • 文档对象模型 (DOM) 是 HTML 和 XML 文档的编程接口。它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。任何 HTML或XML文档都可以用 DOM表示为一个由节点构成的层级结构

创建节点:

  • createElement:创建新元素,接受一个参数,即要创建元素的标签名
  • createTextNode:创建一个文本节点
  • createDocumentFragment:用来创建一个文档碎片,它表示一种轻量级的文档,主要是用来存储临时节点,然后把文档碎片的内容一次性添加到DOM中
  • createAttribute:创建属性节点,可以是自定义属性

查询节点:

  • querySelector:传入任何有效的css 选择器,即可选中单个 DOM元素(首个)
  • querySelectorAll:返回一个包含节点子树内所有与之相匹配的Element节点列表,如果没有相匹配的,则返回一个空节点列表
  • getElementById(‘id’): 返回拥有指定id的对象的引用
  • getElementsByClassName(‘class’): 返回拥有指定的对象集合
  • getElementsByTagName(‘标签名’):返回拥有指定标签名的对象集合
  • getElementsByName(‘name属性值’): 返回拥有指定名称的对象结合

更新节点:

  • innerHTML: 不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树
  • innerText: 自动对字符串进行HTML编码,保证无法设置任何HTML标签, 不返回隐藏元素的文本
    textContent: 自动对字符串进行HTML编码,保证无法设置任何HTML标签, 返回所有文本
  • style: DOM节点的style属性对应所有的CSS,可以直接获取或设置。遇到需要转化为驼峰命名

添加节点:

  • innerHTML:不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树
  • appendChild:把一个子节点添加到父节点的最后一个子节点
  • insertBefore:把子节点插入到指定的位置,子节点会插入到referenceElement之前
  • setAttribute:在指定元素中添加一个属性节点,如果元素中已有该属性改变属性值

删除节点:

  • removeChild:删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的removeChild把自己删掉

2、attribute 和 property 的区别

  • property 形式:是通过 JS 获取修改对象属性的方式从而改变页面样式,不会体现到 html 结构中
const box = document.getElementById('box')
console.log(box.style.width)
  • Attribute 可以通过 setAttribute() 和 getAttribute() 修改 html 属性,会改变 html 结构
var oBox = document.getElementsByClassName('box');
oBox.setAttribute('data-n', 10);
var n = oBox.getAttribute('data-n');
  • 两者都有可能引起 DOM 重新渲染

3、DOM 操作的性能优化

  • DOM 操作非常“昂贵”,避免频繁的 DOM 操作
  • 对 DOM 查询做缓存:可以在循环之前就将主节点,不必循环的Dom节点先获取到,那么在循环里就可以直接引用,而不必去重新查询
// 不缓存 DOM 查询
for(let i =0; i < document.getElementsByTagName('p').length; i++) {
	// 每次循环,都会计算 length 值,频繁进行 DOM 查询
}

// 缓存 DOM 查询
const pList = document.getElementsByTagName('p')
const length = pList.length
for(let i =0; i < length; i++) {
	// 缓存 length,只进行一次 DOM 查询
}
  • 将频繁操作改为一次性操作:利用document.createDocumentFragment()方法创建文档碎片节点,创建的是一个虚拟的节点对象,向这个节点添加dom节点,修改dom节点并不会影响到真实的dom结构,可以先将我们需要修改的dom一并修改完,保存至文档碎片中,然后用文档碎片一次性的替换真实的dom节点
// 频繁操作
const ul1 = document.getElementById('ul1')

for(let i = 0; i < 10; i++) {
	const li = document.createElement('li')
	li.innerHTML = `list item ${i}`
	list.appendChild(li)
}

// 先放入文档片段,然后再一次性插入
const ul1 = document.getElementById('ul1')

// 创建一个文档片段,此时还没有插入到 DOM 结构中
const frag = document.createFragment()

for(let i = 0; i < 10; i++) {
	const li = document.createElement('li')
	li.innerHTML = `list item ${i}`
	// list.appendChild(li)
	// 先插入文档片段中
	frag.appendChild(li)
}
// 都插入文档片段后,再一次性插入到 DOM 结构中
list.appendChild(frag)
  • 用innerhtml代替高频的appendChild
  • 虚拟dom

4、说说减少DOM数量的方法?

  • 可以使用伪元素,阴影实现的内容尽不使用DOM实现,如清除浮动、样式实现等
  • 按需加载,减少不必要的渲染
  • 结构合理,语义化标签,减少代码

5、Javascript的原生对象、内置对象、宿主对象?

  • 原生对象:独立于宿主环境的 ECMAScript 实现提供的对象
    avaScript中的原生对象有Object、Function、Array、String、Boolean、Math、Number、Date、RegExp、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError和Global。
  • 内置对象:由 ECMAScript 实现提供的、独立于宿主环境的所有对象,在 ECMAScript 程序开始执行时出现
    目前定义的内置对象只有两个:Global 和 Math
  • 宿主对象:由 ECMAScript 实现的宿主环境提供的对象
    所有的 BOM 和 DOM 对象都是宿主对象

六、BOM 操作

  • navigator:浏览器信息
  • screen:屏幕信息
  • location:URI 地址信息
  • history:网页前进/后退信息

1、说说你对BOM的理解?常见的BOM对象有哪些?

  • BOM (Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象。其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退、前进、刷新、浏览器的窗口发生变化、滚动条的滚动、以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率

Window:

  • open:既可以导航到一个特定的url,也可以打开一个新的浏览器窗口
  • close:仅用于通过关闭 window.open() 打开的窗口

location:

  • location对象主要用来获取url的信息
  • hash:url中#后面的字符,没有则返回空串
  • host:域名和端口号
  • hostname:域名,不带端口号
  • href:完整url
  • pathname:服务器下面的文件路径
  • port:url的端口号,没有则为空
  • protocol:使用的协议
  • search:url的查询字符串,通常为?后面的内容
  • reload():此方法可以重新刷新当前页面。这个方法会根据最有效的方式刷新页面,如果页面自上一次请求以来没有改变过,页面就会从浏览器缓存中重新加载
// location
console.log(location.href)          // 整个网址
console.log(location.protocal)      // 协议:'http:' 'https:'
console.log(location.host)          // 域名:www.baidu.com
console.log(location.pathname)      // 路径:/api/list
console.log(location.search)        // 参数:"?a=100&b=200"
console.log(location.hash)          // 哈希:"#Login"

navigator:

  • navigator 对象主要用来获取浏览器的属性,区分浏览器类型
// navigator
const ua = navigator.userAgent
const isChrome = ua.indexOf('Chrome')
console.log(isChrome)

screen:

  • 保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度
// screen
console.log(screen.width)
console.log(screen.height)

history:

  • history对象主要用来操作浏览器URL的历史记录,可以通过参数向前,向后,或者向指定URL跳转
  • history.go():接收一个整数数字或者字符串参数:向最近的一个记录中包含指定字符串的页面跳转
  • history.forward():向前跳转一个页面
  • history.back():向后跳转一个页面
  • history.length:获取历史记录数
// history
history.back()       // 回退
history.forward()    // 前进

七、事件

1、事件模型

  • 事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型

事件流三个阶段:

  • 事件捕获阶段(capture phase)
  • 事件目标阶段(target phase)
  • 事件冒泡阶段(bubbling phase)

事件模型:

  • 原始事件模型(DOM0级)
    这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过 js 属性来指定监听函数。原始事件模型方式是所有浏览器都兼容的
  • IE事件模型(基本不用)
    在该事件模型中,一次事件共有两个过程,事件处理阶段,和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行
  • 标准事件模型(DOM2级)
    在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行(true 表示在捕获阶段)

执行顺序的问题:

  • 如果 DOM 节点同时绑定了两个事件监听函数,一个用于捕获,一个用于冒泡,那么两个事件的执行顺序真的是先捕获在冒泡吗,答案是否定的, 绑定在被点击元素的事件是按照代码添加顺序执行的,其他函数是先捕获再冒泡

2、描述一下事件冒泡的流程

  • 当一个事件监听某一个 DOM 元素后,触发该元素或其元素的后代元素,都能使该监听事件被触发
  • 阻止事件冒泡:e.stopPropagation()

3、什么是事件代理(或称事件委托)

  • 事件代理(Event Delegation),又称之为事件委托。是 JavaScript 中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。使用事件代理的好处是可以提高性能
  • 事件代理的原理是:DOM元素的事件冒泡
  • 举例:最经典的就是ul 和li 标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在 li 标签上直接添加,而是在ul 父元素上添加
  • 好处:比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制

4、说说你对事件循环的理解?

  • JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,而实现单线程非阻塞的方法就是事件循环。在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
  • 在JavaScript中,所有的任务都可以分为同步任务和异步任务。同步任务也就是立即执行的任务,同步任务一般会直接进入到主线程中执行。异步任务是异步执行的任务,比如ajax网络请求,setTimeout定时函数等
  • 在执行同步任务的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当异步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。任务队列可以分为宏任务队列和微任务队列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。
  • 微任务包括了 promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。
  • 宏任务包括了 script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲染等。

5、window.onload() 和 DOMContentLoaded 的区别?

  • 当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了。
  • 当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片,flash 等

6、Object.defineProperty()

语法:
Object.defineProperty(obj, prop, descriptor)

参数:

  • obj:必需,目标对象
  • prop:必需,需定义或修改的属性的名字
  • descriptor:必需,将被定义或修改的属性的描述符,是一个对象

descriptor参数解析:

  • 函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符
  • 数据描述:当修改或定义对象的某个属性的时候,给这个属性添加一些特性,数据描述中的属性都是可选的
  • 存取描述:当使用存取器描述属性的特性的时候,允许设置以下特性属性

数据描述:

  • value:属性对应的值,可以使任意类型的值,默认为undefined
  • writable:属性的值是否可以被重写。设置为true可以被重写;设置为false,不能被重写。默认为false
  • enumerable:此属性是否可以被枚举(使用for…in或Object.keys())。设置为true可以被枚举;设置为false,不能被枚举。默认为false
  • configurable:是否可以删除目标属性或是否可以再次修改属性的特性(writable, configurable, enumerable)。设置为true可以被删除或可以重新设置特性;设置为false,不能被可以被删除或不可以重新设置特性。默认为false。这个属性起到两个作用:1、目标属性是否可以使用delete删除 2、目标属性是否可以再次设置特性

存取描述:

  • get:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为 undefined。
  • set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined。

7、mouseover 和 mouseenter 的区别

  • mouseover(产生冒泡):当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是 mouseout
  • mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是 mouseleave

8、如何让事件先冒泡后捕获

  • 在DOM 标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获之间。

9、说一下图片的懒加载和预加载

  • 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。
  • 懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。
  • 两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

10、编写一个通用的事件监听函数

// 通用事件绑定函数
function bindEvent(elem, eventType, fun) {
	elem.addEventListener(eventType, fun)
}

const btn = document,getElementById('btn1')
bindEvent('click', (e) => {
	// console.log(e.target)
	e.preventDefault()   // 阻止默认行为,如:阻止点击链接时跳转
	alert('clicked')
})

11、无限下拉的图片列表,如何监听每个图片的点击?

  • 事件代理
  • 用 e.target 获取触发元素
  • 用 matches 来判断是否是出发元素

八、Ajax

1、什么是浏览器同源策略?

  • 同源策略,它是由Netscape提出的一个著名的安全策略。现在所有支持JavaScript 的浏览器都会使用这个策略
  • 所谓同源是指,域名,协议,端口相同

2、ajax原理是什么?如何实现?

  • AJAX全称(Async Javascript and XML),即异步的JavaScript 和XML,是一种创建交互式网页应用的网页开发技术,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页

Ajax的原理:

  • 简单来说通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面

XMLHttpRequest:

  • Ajax 想要实现浏览器与服务器之间的异步通信,需要依靠 XMLHttpRequest 创建一个对象,XMLHttpRequest 是一个构造函数

readyState 的值:

  • xhr.readyState 这个状态的变化值从 0~4 表示 5 种状态
  • 0:请求还未初始化。尚未调用 open()方法
  • 1:启动(请求已建立)。已经调用 open()方法,但未调用 send() 方法
  • 2:请求已发送。已经调用 send() 方法,但未接收到响应
  • 3:接收。开始接收响应数据
  • 4:接收完成。已经全部接收完响应数据,可以在浏览器中使用了

status 的值:

  • 1xx:收到请求
    100:客户必须继续发送请求
    101:客户要求服务器根据请求转换HTTP协议版本
  • 2xx:表示成功处理请求,如:200
    200:成功处理请求
    201:提示知道新文件的URL
    202:接受和处理、但处理未完成
    203:返回信息不确定或不完整
    204:请求收到,但返回信息为空
    205:服务器完成了请求,用户代理必须复位当前已经浏览过的文件
    206:服务器已经完成了部分用户的GET请求
  • 3xx:需要重定向,浏览器直接跳转
    300:请求的资源可在多处得到
    301:永远重定向到该资源
    302:临时重定向到该资源
    303:建议客户访问其他URL或访问方式
    304:资源未改变,如果访问的资源没有改变,服务器返回304,浏览器使用缓存的资源
    305——请求的资源必须从服务器指定的地址得到
    306——前一版本HTTP中使用的代码,现行版本中不再使用
    307——申明请求的资源临时性删除
  • 4xx:客户端请求错误
    400:错误请求,如语法错误
    401:请求授权失败
    402:保留有效ChargeTo头响应
    403:客户端没有权限
    404:请求地址错误或者服务的没有对应接口
  • 5xx:服务端错误
    500:服务器产生内部错误
    501:服务器不支持请求的函数

创建 ajax 过程:

  • 创建 XMLHttpRequest 对象,也就是创建一个异步调用对象
  • 创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息
  • 设置响应HTTP请求状态变化的函数
  • 发送HTTP请求
  • 获取异步调用返回的数据
  • 使用JavaScript和DOM实现局部刷新

Ajax 的使用步骤:

  • 创建 XMLHttpRequest 的对象
    let xhr = new XMLHttpRequest();

    // 一般使用下面这种方式创建
    let request;
    if (window.XMLHttpRequest) {
        // 这种方式是对于使用这几种浏览器的情况
        // IE7+,Firefox,Chrome,Opera,Safari ...
        request = new XMLHttpRequest(); 
    } else {
        // 这种方式是针对低版本浏览器:IE6,IE5
        request = new ActiveObject("Microsoft.XMLHTTP");
    }
  • 准备发送请求
    // method 是 HTTP 请求方法:GET、POST、PUT、DELETE
    // URL 地址:
    // async 是否使用异步:true 或者 false
    xhr.open(method, url, async);
  • 监听事件,处理响应
    // 当获取到响应后,会触发 xhr 对象的 readystatechange 事件,
    // 可以在该事件中对响应进行处理
    xhr.onreadystatechange = function() {
		// readyState 这个状态的变化值从 0~4 表示 5 种状态
    	// 0:请求还未初始化。尚未调用 open()方法
    	// 1:启动(请求已建立)。已经调用 open()方法,但未调用 send() 方法
    	// 2:请求已发送。已经调用 send() 方法,但未接收到响应
    	// 3:请求处理中。开始接收响应数据但未完成,即可以接收到部分响应数据
    	// 4:接收完成。已经全部接收完响应数据,可以在浏览器中使用了
    	// status == 200 表示请求成功
        if (xhr.readyState == 4 && xhr.status == 200) {
            //将返回结果以文本(字符串)形式输出
            document.write(xhr.responseText);
            //将返回结果以XML形式输出
            //docunment.write(xhr.responseXML);
            // or lists = xhr.responseText
        }
    }
  • 发送请求:调用 send() 方法正式发送请求
    // send() 的参数是通过请求体携带的数据
    // 如果是 GET 请求,括号里填 null 或者不填
    xhr.send();

3、如何解决跨域问题

JSONP:

  • 原理是:动态插入script标签,通过script标签引入一个js文件,这个js文件载入成功后会执行我们在 url 参数中指定的函数,并且会把我们需要的 json 数据作为参数传入。
  • 由于同源策略的限制,XmlHttpRequest只允许请求当前源(域名、协议、端口)的资源,为了实现跨域请求,可以通过 script 标签实现跨域请求,然后在服务端输出JSON数据并执行回调函数,从而解决了跨域的数据请求。
  • 优点是兼容性好,简单易用,支持浏览器与服务器双向通信。缺点是只支持GET请求。

CORS:

  • CORS 是跨域资源分享(Cross-Origin Resource Sharing)的缩写
  • 服务器端对于 CORS 的支持,主要就是通过设置 Access-Control-Allow-Origin来进行的,如果浏览器检测到相应的设置,就可以允许Ajax进行跨域的访问。
// 第二个参数填写允许跨域的域名称,不建议直接写 ‘*’
response.setHeader("Access-Control-Allow-Origin", "http://localhost:8082")
 
// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true"); 
 
// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");

通过修改 document.domain 来解决相同主域下跨子域无法读取非同源网页的 Cookie 问题

  • 因为浏览器是通过document.domain 属性来检查两个页面是否同源,因此只要通过设置相同的document.domain,两个页面就可以共享Cookie(此方案仅限主域相同,子域不同的跨域应用场景。)

使用HTML5中新引进的window.postMessage() 方法来跨文档通信

4、XML和JSON的区别?

  • 数据体积方面:JSON相对于XML来讲,数据的体积小,传递的速度更快些
  • 数据交互方面:JSON与JavaScript的交互更加方便,更容易解析处理,更好的数据交互
  • 数据描述方面:JSON对数据的描述性比XML较差
  • 传输速度方面:JSON的速度要远远快于XML

九、存储

1、说一下 Cookie

  • Cookie 是一种浏览器存储数据的一种方式(全称 HTTP Cookie)
  • Cookie 常用于跟踪统计用户访问网站的习惯,比如什么时间访问了哪些页面,在每个页面停留的时间等

2、Cookie 和 Session 的区别?

  • Cookie 数据存放在客户的浏览器上;Session 数据放在服务器上
  • Cookie 不是很安全,别人可以分析存放在本地的 Cookie 进行 Cookie 欺骗,考虑到安全应当使用 Session
  • 单个 Cookie 保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个Cookie;而 Session 相对来说存储的数据量更大

3、cookie、localStorage、sessionStorage 三者的区别

  • cookie 数据在每次的同源 http 请求中都会携带(即使不需要);而 localStorage 和 sessionStorage 不会自动把数据发送给服务器,仅在本地保存
  • cookie 存储的数据不能超过4Kb;localStorage 和 sessionStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到 5Mb 或更大
  • cookie 只在设置的 cookie 过期时间之前有效,即使窗口关闭或浏览器关闭;localStorage 用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期;sessionStorage不是一种持久化的本地存储,仅在当前浏览器窗口关闭之前有效
  • cookie 和 localstorage 在所有同源窗口中都是共享的;sessionStorage 不能在不同的浏览器窗口中共享,即使是同一个页面
  • cookie 不支持事件通知机制;localStorage 和 sessionStorage 支持事件通知机制,可以将数据更新的通知发送给监听者
  • localStorage 和 sessionStorage 的 API 接口使用更方便

应用场景:

  • 标记用户与跟踪用户行为的情况,推荐使用cookie
  • 适合长期保存在本地的数据(令牌),推荐使用localStorage
  • 敏感账号一次性登录,推荐使用sessionStorage
  • 存储大量数据的情况、在线文档(富文本编辑器)保存编辑历史的情况,推荐使用indexedDB

十、节流和防抖

1、手写节流 throttle

  • 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效
  • 应用场景:节流可以使用在 scroll 函数的事件监听,或者拖拽事件,通过事件节流来降低事件调用的频率
function throttle(fn, wait = 200) {
	// timer 是在闭包中
	let timer = null
	
	return function() {
		if(timer) return
		
		timer = setTimeout(() => {
			fn.apply(this, arguments)
			timer = null
		}, wait)
	}
}

2、手写防抖 debounce

  • 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时
  • 应用场景:可以使用在一些点击请求的事件,或按键输入change事件,避免因为用户的多次点击向后端发送多次请求
function debounce(fn, wait = 300) {
	// timer 是在闭包中
	let timer = null
	
	return function() {
		if(timer) clearTimeout(timer)
		
		timer = setTimeout(() => {
			fn.apply(this, arguments)
			timer = null
		}, wait)
	}
}

十一、Webpack 和 Babel

1、前端为什么要进行打包和构建?

  • 体积更小,加载更快(tree-shaking,压缩合并)
  • 可以编译高级语言和语法(如:TypeScript,ES6,模块化,SCSS)
  • 兼容性和错误提示(polyfill,postcss,eslint)
  • 统一开发环境
  • 统一的构建流程和产出标准

2、谈谈你对 Webpack 的理解?

  • Webpack 是一个模块打包工具,可以使用它管理项目中的模块依赖,并编译输出模块所需的静态文件。它可以很好地管理、打包开发中所用到的 HTML、CSS、JavaScript 和静态文件(图片,字体)等,让开发更高效。对于不同类型的依赖,Webpack 有对应的模块加载器,而且会分析模块间的依赖关系,最后合并生成优化的静态资源

3、Babel 和 Webpack 的区别?

  • babel:JS 新语法编译工具,不关心模块化
  • webpack:打包构建工具,是多个 loader、plugin 的集合

4、babel-polyfill 和 babel-runtime 的区别?

  • babel-polyfill 会污染全局
  • babel-runtime 不会污染全局
  • 产出第三方 lib 要用 babel-runtime

5、Webpack 五个核心概念分别是什么?

  • mode:模式(Mode)指示 Webpack 使用相应模式的配置,只有development(开发环境)和 production(生产环境)两种模式
  • entry:入口(Entry)指示 Webpack 以哪个文件为入口起点开始打包,分析内部构件依赖图
  • output:输出(Output)指示 Webpack 打包后的资源 bundles 输出到哪里去,以及如何命名
  • loader:Loader 能让 Webpack 处理非 JavaScript/json 文件(Webpack 自身只能处理 JavaScript/json )
  • plugins:插件(Plugins)可以用于执行范围更广的任务,包括从打包优化和压缩到重新定义环境中的变量

6、module、chunk、bundle 区别?

  • module:Webpack 中一切皆模块,各个源文件都是一个module;
  • chunk:webpack 打包时将入口文件所依赖的模块都引入到入口文件而形成的文件就是chunk;
  • bundle:webpack 打包后最终的输出形成的文件就是bundle。

7、Loader 和 Plugin 的区别?

不同的作用:

  • Loader 译为"加载器",作用是让 webpack 拥有加载和解析非JavaScript文件的能力。Loader 用于对模块的源代码进行转换,可以使在 import 或 “load(加载)” 模块时预处理文件
  • Plugin 译为"插件",Plugin 作用是扩展 webpack 的功能,让 webpack 具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果

不同的用法:

  • Loader 在 module.rules 中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个 Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
  • Plugin 在 plugins 中单独配置。 类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入

8、有哪些常见的Loader?它们是解决什么问题的?

  • file-loader:将文件发送到输出文件夹,并返回(相对)URL
  • url-loader:像 file loader 一样工作,但如果文件小于限制,可以返回 data URL
  • style-loader:将模块的导出作为样式添加到 DOM 中
  • css-loader:解析 CSS 文件后,使用 import 加载,并且返回 CSS 代码
  • sass-loader:将 Sass 编译成 CSS,默认使用 Node Sass
  • babel-loader:加载 ES6+ 代码,然后使用 Babel 转译为 ES5
  • html-loader:导出 HTML 为字符串,需要引用静态资源
  • vue-loader:加载和转译 Vue 组件
  • eslint-loader:通过 ESLint 检查 JavaScript 代码

9、有哪些常见的Plugin?它们是解决什么问题的?

  • html-webpack-plugin:会以指定 html 文件为模版,在打包结束后,自动生成一个 html 文件,并把打包生成的 JS 自动引入到这个 html 文件中
  • clean-webpack-plugin:重新打包之前自动清空 dist 目录

10、css-loader 和 style-loader 的区别

  • css-loader 处理 css 文件
  • style-loader 把 js 中 import 导入的样式文件代码,打包到 js 文件中,运行 js 文件时,将样式自动插入到 style 标签中
  • 同一个 rule 下,loader 的执行顺序是从后到前,从右到左

11、file-loader 和 url-loader 的区别?

  • file-loader 返回的是图片的url
  • url-loader 可以通过 limit 属性对图片分情况处理,当图片小于 limit(单位: byte )大小时转 base64 ,大于 limit 时调用 file-loader 对图片进行处理
  • url-loader 封装了 file-loader,但 url-loader 并不依赖于 file-loader

12、ES Module 和 CommonJS 的区别?

  • 语法上不同:
    ES Module 使用的是 export 或 export default 进行导出,用 import 进行导入(一个文件内可以有多个 export 进行导出,但 import 引入时需要加大括号且不能取别名 )
    Common JS 使用的是 module.exports 导出,require 进行导入
  • 使用方式不同:
    ES Module 主要是用在浏览器
    Common JS 主要用于服务端
  • 引入时不同:
    ES Module 是静态引入,也就是编译时引入
    Common JS 是动态引入,运行时需要了才引入

13、Webpack 的热更新是如何做到的?说明其原理?

  • Webpack 的热更新又称热替换(Hot Module Replacement),缩写为HMR,基于devServer, 这个机制可以做到不用刷新浏览器而将修改后的模块替换掉旧的模块,打包时只会打包修改的模块而不是对所有模块进行打包,极大提升构建速度。

原理:

  • 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中;
  • 第二步是 webpack-dev-server 和 webpack 之间的接口交互,这里的交互主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API 对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  • 第三步是 webpack-dev-server 对文件变化的一个监控,当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行刷新;
  • 第四步:webpack-dev-server 的依赖 sockjs 在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,浏览器端根据这些 socket 消息进行不同的操作;
  • 第五步:webpack-dev-server/client 将接收到服务端的消息传给 webpack/hot/dev-server,webpack/hot/dev-server 根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器还是进行模块热更新;如果是刷新浏览器就直接刷新浏览器,如果热更新则执行下一步;
  • 第六步:HotModuleReplacement.runtime 接收到 webpack-dev-server/client 传递给他的新模块的 hash 值,然后通过 jsonp 请求,获取到最新的模块代码;
  • 第七步:HotModulePlugin 会对新旧模块进行对比,如果决定更新模块,则在更新模块前检查模块之间的依赖关系,然后执行更新操作。

14、webpack 的构建流程是什么?

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  • 确定入口:根据配置中的 entry 找出所有的入口文件;
  • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  • 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

15、如何提高 webpack 的构建速度?

  • 使用 CommonsChunkPlugin 提取公共的模块,可以减小文件体积,也有助于浏览器层的文件缓存;
  • 通过 externals 配置来提取常用库;
  • 利用 DllPlugin 和 DllReferencePlugin 预编译资源模块,通过 DllPlugin 来对那些引用但绝对不会修改的 npm 包进行预编译,再通过 DllReferencePlugin 将预编译的模块加载进来;(这种方式和externals是类似的,主要用于有些模块没有可以在 script 标签中引入的资源纯(npm包))
  • 使用Happypack 实现多线程加速编译,不过 Happypack 对 file-loader 和 url-loader 的支持不够友好,所以这两个loader就不需要换成 happypack 了,其他loader 可以类似地换一下
  • 使用 webpack-uglify-parallel-plugin 代替自带的 UglifyjsWebpackPlugin 插件来提升压缩速度 (自带的 UglifyjsWebpackPlugin 压缩插件是单线程执行的,而webpack-parallel-uglify-plugin可以并行的执行)
  • 使用 fast-sass-loader 代替 sass-loader(ast-sass-loader可以并行地处理sass,在提交构建之前会先组织好代码,速度也会快一些)
  • 使用Tree-shaking 和 Scope Hoisting 来剔除多余代码
  • babel-loader开启缓存(babel-loader 在执行的时候,可能会产生一些运行期间重复的公共文件,造成代码体积大冗余,同时也会减慢编译效率,可以加上cacheDirectory参数或使用 transform-runtime 插件)
  • devtool 的参数在开发环境设置为 cheap-module-eval-source-map;在生产环境时设置为 cheap-module-source-map

16、如何利用webpack来优化前端性能?(优化产出代码)

  • 用 webpack 优化前端性能是指优化 webpack 的输出结果,让打包的最终结果在浏览器运行快速高效。
  • 删除注释、简化代码的写法等;
  • 使用 Tree Shaking 将使用不到的代码片段剔除掉,可以在 optimization 中配置 usedExports: true 或者在启动webpack时追加参数–optimize-minimize 来实现(Tree Shaking 只支持 ES Module 方式的引入)
  • 利用 webpack 的 UglifyjsWebpackPlugin 和 WebpackUglifyParallelPlugin 来压缩 JS 文件,利用 OptimizeCssAssetsPlugin 来压缩 CSS 文件
  • 可以利用 webpack 的 output 参数和 loader 的 publicPath 参数来将静态资源路径修改为CDN 上对应的路径

17、webpack 如何实现懒加载?

  • 懒加载是异步加载某个模块,只有在使用到的时候才会加载
  • webpack 使用 Prefetching 和 Preloading 实现懒加载,使用的方式是在使用 import() 异步加载某个模块时,对加载的模块加上魔法注释,注释内容为:webpackPrefetch: true 或者 webpackPreload: true
import(/* webpackPrefetch: true */ 'lodash')
  • Prefetching/Preloading 实现懒加载主要区别是:prefetching 的代码会等到主要的 js 代码加载完,浏览器空闲时才会去加载(例如首页中的登录注册模块),preloading 代码会与主要的 js 代码一起加载

18、Webpack proxy工作原理?为何能实现跨域?

  • 在开发阶段, webpack-dev-server 会启动一个本地开发服务器,所以我们的应用在开发阶段是独立运行在 localhost的一个端口上,而后端服务又是运行在另外一个地址上
  • 所以在开发阶段中,由于浏览器同源策略的原因,当本地访问后端就会出现跨域请求的问题
  • 通过设置webpack proxy实现代理请求后,相当于浏览器与服务端中添加一个代理者
  • 当本地发送请求的时候,代理服务器响应该请求,并将请求转发到目标服务器,目标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地
  • 在代理服务器传递数据给本地浏览器的过程中,两者同源,并不存在跨域行为,这时候浏览器就能正常接收数据
  • 注意:服务器与服务器之间请求数据并不会存在跨域行为,跨域行为是浏览器安全策略限制

19、如何在vue项目中实现按需加载?

  • Vue UI 组件库的按需加载,如 Element UI 需要安装 babel-plugin-component 插件,AntDesign 安装 babel-plugin-import 插件后,在 .babelrc 配置中或 babel-loader 的参数中设置 useBuiltIns: ‘usage’ ,即可实现组件按需加载了;
  • 单页应用的按需加载可以通过 import() 语句来控制加载时机,webpack 内置了对于 import() 的解析,会将 import()中引入的模块作为一个新的入口在生成一个 chunk,当代码执行到 import() 语句时,会去加载 Chunk 对应生成的文件,import() 会返回一个 Promise 对象,所以为了让浏览器支持,需要事先注入Promise polyfill

十二、其它

1、ES6 新增哪些东西?

  • 新增模板字符串
  • 箭头函数
  • for-of 用来遍历数据数据
  • arguments 对象可被不定参数和默认参数完美代替
  • 将 promise 对象纳入规范,提供了原生的 Promise 对象。
  • 增加了 let 和 const 关键字用来声明变量,增加了块级作用域
  • 还有就是引入 module 模块的概念

2、栈和队列的区别?

  • 栈先进后出,插入和删除操作都是在一端进行,可以用 push 和 pop、unshift 和 shift 模拟
  • 队列先进先出,而队列的插入和删除在不同端进行,可以用 push 和 shift、unshift 和 pop 模拟

3、栈和堆的区别?

  • 栈区(stack): 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等,值类型的数据存放在栈区中;
  • 堆区(heap) : 一般由程序员分配与释放,若不释放,程序结束时可能由 GC 回收,引用类型的数据存放在堆中,数据的引用存放在栈区。

4、Javascript 垃圾回收方法?

怎么判断对象是否可以被回收:

  • 引用计数算法:引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其他对象引用,则它的引用计数加1,如果删除该对象的引用,那么引用计数就减1,当该对象的引用计数为0时,那么该对象就可以被回收。
  • 可达性分析算法
    ??可达性分析算法(Reachability Analysis)的基本思路是从GC Roots开始向下搜索,搜索走过的路径被称为引用链,当一个对象与GC Roots没有任何引用链相连时,则证明该对象是可以回收的。
    ??通过可达性分析算法成功解决了引用技术算法无法解决的“循环依赖”的问题。
    ??只要对象无法与GC Root建立直接或间接的链接,系统就会判定该对象为可回收对象。

垃圾回收算法:

  • 标记-清除算法:标记-清除算法(Mark-Sweep)是把内存区域中的这些对象进行标记,把属于可回收标记出来,然后把这些可回收的垃圾清除掉。
    ??缺点:效率不高,无法清除垃圾碎片。
  • 复制回收算法:复制回收算法是将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完,就将还存活的对象复制到另一块上面,然后将已使用满的内存空间一次清理掉,保证了内存的连续可用。
    ??优点:解决标记-清除算法的内存碎片问题,逻辑清晰,运行高效。
    ??缺点:只能利用一半内存。
  • 标记整理算法:标记-整理算法(Mark-Compact)标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。
    ??优点:解决内存碎片的问题,也规避了复制回收算法只能利用一半内存的问题。
  • 分代回收算法:分代回收算法(Generational Collection)是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。
    ??在新生代中,每次垃圾回收时都发现有大批量对象死去,只有少量存活,那就选用复制回收算法,只需要付出少量存活对象的复制成本就可以完成收集。
    ??而老年代中因为对象存活率高、没有额外的空间对它们进行分配,就必须使用标记-清除算法或标记-整理算法来进行回收。

5、Iframe 有哪些缺点?

  • iframe 会阻塞主页面的 onLoad 事件(过多会增加服务器的HTTP请求);
  • 搜索引擎的检索程序无法解读这种页面,不利于SEO;
  • iframe 和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载;
  • 如果需要使用 iframe,最好是通过 javascript 动态给 iframe 添加 src 属性值,这样可以绕开以上的问题。

6、JavaScript 标签的 async 和 defer 属性的区别?

  • 两者共同点都是使脚本异步加载
  • defer 属性在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞(多个设置了 defer 属性的脚本是顺序执行的)
  • async 属性当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞(多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行)
  • 当一个 script 标签内同时包含 defer 与 async 属性时,只会触发 async ,不会触发 defer ,除非浏览器不兼容 async

7、一次JS 请求一般会有哪些地方有缓存处理?

  • DNS 缓存:短时间内多次访问某个网站,在限定时间内,不用多次访问 DNS 服务器;
  • CDN缓存:CDN 是Content Delivery NetWork的简称,即‘内容分发网络’,当用户浏览网站请求数据时,CDN会选择一个离用户最近的CDN边缘节点来响应用户的请求。CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低,同时起到了分流作用,减轻了源站的负载。
  • 浏览器缓存:浏览器在用户磁盘上,对最新请求过的文档进行了存储;
  • 服务器缓存:将需要频繁访问的 Web 页面和对象保存在离用户更近的系统中,当再次访问这些对象的时候加快了速度。

8、jQuery特点?

  • jQuery 是轻量级的 JS 库 ,它兼容CSS3,还兼容各种浏览器,jQuery 使用户能更方便地处理 HTML、events、实现动画效果,并且方便地为网站提供 AJAX 交互,而且它的文档说明很全,而且各种应用也说得很详细,同时还有许多成熟的插件可供选择;

jQuery特点:

  • jQuery 是在js基础上进行的封装,可以相互转换,不是全新的语言;
  • jQuery 最核心的理念就是“用最少的代码,做最多的事情”“write less do more”;
  • jQuery 最大特点就是具有强大的兼容性,不在为各种浏览器兼容性问题而费神费力;
  • jQuery 使用的是链式写法,可以把多行代码写在一行,方便简洁;
  • jQuery 还简化了 JS 操作 CSS 的代码,并且代码的可读性也比js要强;
  • jQuery 简化了 AJAX 操作,后台只需返回一个JSON 格式的字符串就能完成与前台的通信;
  • jQuery 提供了扩展接口:JQuery.extend(object),可以在 jQuery 的命名空间上增加新函数(JQuery的所有插件都是基于这个扩展接口开发的);
  • jQuery 有着丰富的第三方的插件,例如:树形菜单、日期控件、图片切换插件、弹出窗口等等,基本前台页面上的组件都有对应插件,并且用 jQuery 插件做出来的效果很炫,并且可以根据自己需要去改写和封装插件,简单实用。

9、JavaScript中内存泄漏的几种情况?

  • 内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成不再使用的程序未能释放

常见的内存泄漏:

  • setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏;
  • 闭包是存放在内存中的,大量的闭包会造成内存泄露;
  • 未清理控制台的日志也会引发内存泄露;
  • 循环(在两个对象彼此引用且彼此保留时,就会产生一个循环)

解决内存泄漏:

  • setTimeout第一个参数为函数
  • 使用严格模式,可以避免意外的全局变量
  • 及时清理 dom 元素, dom = null

10、说说你对 MVC 和 MVVM 的理解

在 MVC 模式中:

  • Model:负责保存应用数据,与后端数据进行同步
  • Controller: 负责处理业务逻辑,根据用户行为对 Model 数据进行修改
  • View: 负责视图展示,将 Model 中的数据可视化出来
  • MVC 模式通信是单向的:View -> Controller -> Model -> View

MVVM:

  • 在 MVVM 模式下,View 和 Model 之间没有直接的关系,而是通过 ViewModel 进行交互,Model 和 ViewModel 之间的交互是双向的,因此 View 数据的变化会同步到 Model 中,而 Model 数据的变化也会立即反应到 View 上,ViewModel 只是作为桥梁起到连接 View 和 Model 的作用。

11、setTimeout(fn, 100) 中的 100 毫秒是如何权衡的?

  • 当程序执行到 setTimeout() 函数时,会将函数放入任务列表,当主现场执行到该函数时,会有 100 毫秒的等待时间,这里的 100 毫秒等于插入队列的事件 + 等待的时间

12、setTimeout、setInterval 和 requestAnimationFrame 之间的区别?

  • setTimeout 是以 n 毫秒后执行回调函数,回调函数中可以递归调用 setTimeout 来实现动画;
  • setInterval 以 n 毫秒的间隔时间调用回调函数;
  • 由于开发者对帧数不好把握,HTML5 新增类似于 setTimeout 定时器的 API requestAnimationFrame,它是 window 对象的一个方法,requestAnimationFrame 的基本思想让页面重绘的频率与显示器固定的刷新频率(60Hz 或 75Hz)保持同步,这样就不需要像 setTimeout 那样传递时间间隔,而是浏览器通过系统获取并使用显示器刷新频率对网页进行重绘实现动画;
  • 使用 requestAnimationFrame 执行动画,最大优势是能保证回调函数在屏幕每一次刷 新间隔中只被执行一次,这样就不会引起丢帧,动画也就不会卡顿。

13、document.write() 的用法

原理:

  • 它能够直接在文档流中写入字符串,一旦文档流已经关闭,那么 document.write() 就会重新运用 document.open() 打开新的文档流并写入,此时原来的文档流会被清空,已经渲染好的页面就会被覆盖,浏览器将重新构建 DOM 并渲染新的页面。

document.write()方法可以用在两个方面:

  • 如果能保证能在 onload 前执行,页面载入过程中用实时脚本创建页面内容;
  • 用延时脚本创建本窗口或新窗口的内容。

14、git fetch 和 git pull 的区别?

  • git pull:相当于是从远程获取最新版本并 merge 到本地
  • git fetch:相当于是从远程获取最新版本到本地,不会自动merge

15、JavaScript 数字精度丢失问题

  • 问题:
    ??当计算机计算 0.1+0.2 的时候,实际上计算的是这两个数字在计算机里所存储的二进制,0.1 和 0.2 在转换为二进制表示的时候会出现位数无限循环的情况。js 中是以 64 位双精度格式来存储数字的,只有 53 位的有效数字,超过这个长度的位数会被截取掉这样就造成了精度丢失的问题。这是第一个会造成精度丢失的地方。
    ??在对两个以 64 位双精度格式的数据进行计算的时候,首先会进行对阶的处理,对阶指的是将阶码对齐,也就是将小数点的位置对齐后,再进行计算,一般是小阶向大阶对齐,因此小阶的数在对齐的过程中,有效数字会向右移动,移动后超过有效位数的位会被截取掉,这是第二个可能会出现精度丢失的地方。
    ??当两个数据阶码对齐后,进行相加运算后,得到的结果可能会超过 53 位有效数字,因此超过的位数也会被截取掉,这是可能发生精度丢失的第三个地方。
  • 解决:
    ??对于这样的情况,我们可以将其转换为整数后再进行运算,运算后再转换为对应的小数,以这种方式来解决这个问题;
    ??使用 bigInt() 进行计算。

16、前后端分离?

前后端分离优势:

  • 前端只需要关注页面的样式与动态数据的解析&渲染,而后端专注于具体业务逻辑;
  • 可以实现真正的前后端解耦,前端服务器使用 nginx,可以减少后端服务器的并发/负载压力;
  • nginx 支持页面热部署,不用重启服务器,前端升级更无缝;
  • 支持多端应用;
  • 单个页面显示的东西再多也不怕,因为是异步加载;
  • 后端服务暂时超时或者宕机了,前端页面也会正常访问,只不过数据刷不出来而已;
  • 可以提升开发效率,因为可以前后端并行开发,而不是像以前的强依赖;
  • 在 nginx 中部署证书,外网使用 https 访问,并且只开放 443 和 80 端口,其他端口一律关闭(防止黑客端口扫描),内网使用http,性能和安全都有保障。

后端工程师:

  • 把精力放在java基础,设计模式,jvm原理,spring+springmvc原理及源码,linux,mysql事务隔离与锁机制,mongodb,http/tcp,多线程,分布式架构,弹性计算架构,微服务架构,java性能优化,以及相关的项目管理等等。
  • 后端追求的是:三高(高并发,高可用,高性能),安全,存储,业务等等

前端工程师:

  • 把精力放在 html5,css3,jquery,angularjs,bootstrap,reactjs,vuejs,webpack,less/sass,gulp,nodejs,Google V8引擎,javascript多线程,模块化,面向切面编程,设计模式,浏览器兼容性,性能优化等等。
  • 前端追求的是:页面表现,速度流畅,兼容性,用户体验等等

17、面向过程和面向对象的区别?

  • 面向过程 :分析出解决问题所需要的步骤,强调的是解决问题的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;
  • 面向对象:是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

面向过程优点:

  • 流程化使得编程任务明确,在开发之前基本考虑了实现方式和最终结果,具体步骤清楚,便于节点分析。
  • 效率高,面向过程强调代码的短小精悍,善于结合数据结构来开发高效率的程序。

面向过程缺点:

  • 需要深入的思考,耗费精力,代码重用性低,扩展能力差,后期维护难度比较大。

面向对象优点:

  • 结构清晰,程序是模块化和结构化,更加符合人类的思维方式;
  • 易扩展,代码重用率高,可继承,可覆盖,可以设计出低耦合的系统;
  • 易维护,系统低耦合的特点有利于减少程序的后期维护工作量。

面向对象缺点:

  • 开销大,当要修改对象内部时,对象的属性不允许外部直接存取,所以要增 加许多没有其他意义、只负责读或写的行为。这会为编程工作增加负担,增加运行开销,并且使程序显得臃肿。
  • 性能低,由于面向更高的逻辑抽象层,使得面向对象在实现的时候,不得不做出性能上面的牺牲,计算时间和空间存储大小都开销很大。

总结

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-10-15 11:41:41  更:2021-10-15 11:42:46 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/20 13:00:31-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码