# JS 面试题
# 一、JS 的数据类型及区别
# 1. 数据类型分为两大类:
值类型(简单/基本对象类型)和 引用类型(复杂/复杂对象类型)
基本类型:在内存中占据固定大小,保存在栈内存中。
- Number(数字)
- String(字符串)
- Boolean(布尔)
- undefined(未定义)
- null(空)
- Symbol(符号)(ES6 新增的)
引用类型:引用类型的值是对象,保存在堆内存中,栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址。
- Object(对象) 包括:数组、函数
- Function(函数)
- Array(数组)
- Date(日期)
- RegExp(正则表达式)
- 特殊的基本包装类型(String、Number、Boolean)
- 单体内置对象(Global、Math)...
# 2. 区别:
| 基本类型 | 引用类型 | |
|---|---|---|
| 存储位置 | 存放在栈中,它们的值直接存储在变量访问的位置 | 存放在堆中,存储在变量处的值是一个指针(point),指向存储对象的内存地址 |
| 复制变量 | 会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的 value 而已 | 会把这个内存地址赋值给新变量,也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上,只是多了一个指针 |
基本类型例题:
var a = 10;
var b = a;
b = 20;
console.log(a); // 10值
console.log(b); // 20值
引用类型例题:
var obj1 = new Object();
var obj2 = obj1;
obj2.name = "我有名字了";
console.log(obj1.name); // 我有名字了
// 说明这两个引用数据类型指向了同一个堆内存对象。obj1 赋值给 obj2,实际上这个堆内存对象在栈内存的引用地址复制了一份给了 obj2,
// 但是实际上他们共同指向了同一个堆内存对象,所以修改 obj2 其实就是修改那个对象,所以通过 obj1 访问也能访问的到。
# 二、JS 中数据类型检测方案
# 1. typeof:
优点:能够快速区分基本数据类型
缺点:不能将Object、Array和Null区分,都返回object
console.log(typeof 1); // number
console.log(typeof true); // boolean
console.log(typeof 'hi'); // string
console.log(typeof Symbol) // function
console.log(typeof function(){}); // function
console.log(typeof console.log()); // function
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object
console.log(typeof undefined); // undefined
# 2. instanceof:
优点:能够区分Array、Object和Function,适合用于判断自定义的类实例对象
缺点:Number,Boolean,String基本数据类型不能判断
console.log(1 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
# 3. Object.prototype.toString.call():
优点:精准判断数据类型
缺点:写法繁琐不容易记,推荐进行封装后使用
var toString = Object.prototype.toString;
console.log(toString.call(1)); //[object Number]
console.log(toString.call(true)); //[object Boolean]
console.log(toString.call('hi')); //[object String]
console.log(toString.call([])); //[object Array]
console.log(toString.call({})); //[object Object]
console.log(toString.call(function(){})); //[object Function]
console.log(toString.call(undefined)); //[object Undefined]
console.log(toString.call(null)); //[object Null]
# 三、数组和字符串的方法
# 1. 数组的方法:
| 方法名 | 作用 | 返回值 | 是否改变原数组 |
|---|---|---|---|
| arr.push() | 从后面添加元素 | 添加之后的新数组的长度 | 是 |
| arr.unshift() | 从前面添加元素 | 添加之后的新数组的长度 | 是 |
| arr.shift() | 从前面删除元素,只能删一个 | 删除的元素 | 是 |
| arr.pop() | 从后面删除元素,只能删一个 | 删除的元素 | 是 |
| arr.splice(i,n) | 删除从 i (索引值)开始的 n 个元素 | 删除的元素 | 是 |
| arr.sort() | 将数组进行排序 | 排序的数组(默认是按照最左边的数字进行排序,不是按照数字大小排序的) | 是 |
| arr.forEach(callback) | 遍历数组 | 不管有无 return,都不会返回任何值 | 是 |
| arr.reverse() | 将数组反转 | 反转后的新数组 | 是 |
| arr.concat() | 连接两个数组 | 连接后的新数组 | 否* |
| arr.join() | 将一个数组的所有元素拼接成一个字符串 | 返回拼接的字符串 | 否* |
| arr.slice(start,end) | 切去索引值 start 到索引值 end 的数组,不包含 end 索引的值 | 切出来的新数组 | 否* |
| arr.map(callback) | 映射数组(遍历数组),用一个参数接收 | 有 return 返回一个新数组 | 否* |
| arr.filter(callback) | 过滤(筛选)数组,用一个参数接收 | 返回一个满足要求的数组 | 否* |
# 2. 字符串的方法:
| 方法名 | 作用 | 返回值 | 是否改变原字符串 |
|---|---|---|---|
| str.charAt() | 获取指定位置(索引 i )处的字符 | 如超出有效范围的索引值,返回空字符串 | 否 |
| str.concat() | 拼接两个字符串(可以有多个参数) | 拼接后的新字符串 | 否 |
| str.slice(start,end) | 切去索引值 start 到索引值 end 的字符串,不包含 end 索引的值,[ start , end ) | 切出来的新字符串 | 否 |
| str.indexOf() | 返回指定内容在原字符串中的位置 | 如未找到子字符串,返回-1 | 否 |
| str.lastIndexOf() | 从后往前找,只找第一个匹配的 | 返回指定值最后一次出现的索引,如未找到子字符串,返回-1 | 否 |
| str.trim() | 只能去除字符串前后的空白 | 去掉空白的新字符串 | 否 |
| str.split() | 将字符串转化为数组 | 转化后的数组 | 否 |
| str.toUpperCase() | 转换大写 | 转换为大写的新字符串 | 否 |
| str.toLowerCase() | 转换小写 | 转换为小写的新字符串 | 否 |
| str.replace() | 替换字符串,参数(把什么,改成什么) | 替代的新字符串 | 否 |
# 四、JS 的作用域、作用域链和预解析
# 1. 作用域:
js 的作用域:就是代码名字(变量)在某个范围内起作用和效果。
目的:为了提高程序的可靠性,更重要的是减少命名冲突。
全局作用域: 整个script标签 或者是个单独的js文件。
var num = 30; console.log(num);局部作用域(函数作用域):在函数内就是局部作用域 这个代码的名字只在函数内部起效果和作用。
function fn() { // 局部作用域 var num = 20; console.log(num); } fn();
# 2. 作用域链:
内部函数访问外部函数,采取链式查找的方式来决定取哪个值的结构。
var num = 10;
function fn() { // 外部函数
var num = 20;
function fun() { // 内部函数
console.log(num); // 输出 20
}
fun();
}
fn();
# 3. 预解析:
(1)js 引擎运行 js 分两步:预解析 和 代码执行。
① 预解析:js 引擎会把 js 里面所有的 var 还有 function 提升到当前作用域的最前面。
② 代码执行:按照代码书写的顺序从上往下执行。
(2)预解析分为 变量预解析(变量提升) 和 函数预解析(函数提升)。
① 变量提升:把所有的变量声明提升到当前作用域的最前面 不提升赋值操作。
② 函数提升:把所有的函数声明提升到当前作用域的最前面 不调用函数。
```js
console.log(sum); // 报错
```
# 五、JS 中改变 this 指向的方法
# 1. 函数内 this 指向
- 普通函数调用 : window (node 环境指向:global)
- 立即执行函数 : window (node 环境指向:global)
- 定时器函数 : window (node 环境指向:Timeout 对象)
- 事件绑定方法 : 绑定事件对象
- 对象方法调用 : 该方法所属对象
- 构造函数调用 : 实例对象 (原型对象里面的方法也指向实例对象)
# 2. 改变 this 指向的 call 方法
(1)call() 方法调用一个对象,可以改变 this 指向。
function.call(thisArg, arg1, arg2,……)
// thisArg: 当前调用函数 this 的指向对象
// arg1, arg2: 传递的其他参数
(2)实现一个 call 函数
Function.prototype.myCall = function (context) {
var context = context || window
// 给 context 添加一个属性
// getValue.call(a, 'yck', '24') => a.fn = getValue
context.fn = this
// 将 context 后面的参数取出来
var args = [...arguments].slice(1)
// getValue.call(a, 'yck', '24') => a.fn('yck', '24')
var result = context.fn(...args)
// 删除 fn
delete context.fn
return result
}
# 3. 改变 this 指向的 apply 方法
(1)apply() 方法调用一个函数,可以改变 this 指向。
fun.apply(thisArg, [argsArray])
// thisArg: 在 fun 函数运行时指定的 this 值
// argsArray: 传递的值,必须包含在数组里
// 返回值就是函数的返回值,因为它就是调用函数
(2)实现一个 apply 函数
Function.prototype.myApply = function (context) {
var context = context || window
context.fn = this
var result
// 需要判断是否存储第二个参数
// 如果存在,就将第二个参数展开
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
# 4. 改变 this 指向的 bind 方法
(1)bind() 方法不会调用函数,但能改变 this 指向。
function.bind(thisArg, arg1, arg2,……)
// thisArg: 在 fun 函数运行时指定的 this 值
// arg1, arg2: 传递的其他参数
// 返回由指定的 this 值和初始化参数改造的原函数拷贝
(2)实现一个 bind 函数
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
# 5. 相同点、不同点及应用场景
(1)相同点:都可以改变函数内部的 this 指向。
(2)不同点:
- call 和 apply 会调用函数,并且改变函数内部 this 指向。
- call 和 apply 传递的参数不一样,call 传递参数 aru1,aru2...形式,而 apply 必须数组形式[arg]。
- bind 不会调用函数,但可以改变函数内部 this 指向。
(3)主要应用场景:
- call 经常做继承。
- apply 经常跟数组有关系,比如借助于数学对象实现数组最大值、最小值。
- bind 不调用函数,但还想改变 this 指向时使用,比如改变定时器内部的 this 指向。
# 六、构造函数原型、对象原型、原型链
# 1. 构造函数原型(prototype):
每一个构造函数都有一个 prototype 属性,指向另一个对象。
# 2. 对象原型(proto):
对象都会有一个属性__proto__指向构造函数的 prototype 原型对象。
# 3. 原型链:
通过构造函数创建实例对象,构造函数指向——>这个实例对象,实例对象里面有一个原型__proto__ 指向——>原型对象(prototype),实例对象里面有一个 constructor 可以指回——>构造函数 【实例对象里的 constructor 其实是通过原型对象(prototype) 来指回——>构造函数的】。
原型对象(Star.prototype)里面的__proto__指向——> Object.prototype, Object.prototype 里面的__proto__指向——>null(表示原型链的顶端)。

# 4. 普通函数和构造函数的区别
- 写法:构造函数也是一个普通函数,构造函数首字母大写。构造函数的函数名与类名相同
- 调用方式:普通函数->直接调用,构造函数->用 new 关键字来调用
- 创建对象:普通函数->不会创建新对象,构造函数->调用时,内部会创建一个新的实例对象
- this 指向:普通函数->this 指向调用函数的对象(没调用默认 window),构造函数->this 指向实例对象
- 返回值:普通函数->由 return 语句来决定,构造函数->默认是创建的实例对象
# 七、new 运算符的实现机制
- 在内存中创建一个新的空对象。
- 设置原型,将对象的原型设置为函数的 prototype 对象。
- 让 this 指向这个新对象,执行构造函数里面的代码(给这个新对象添加属性和方法)。
- 判断函数的返回值类型,如:值类型,返回创建的对象,如:引用类型,就返回这个引用类型的对象。
- 利用构造函数创建对象
function Star(uname, age) {
this.uname = uname;
this.age = age;
this.sing = function() {
console.log('我会唱歌');
}
}
var ldh = new Star('刘德华', 18);
// {uname:'刘德华',age: 18, sing : function() { console.log('我会唱歌');}}
var zxy = new Star('张学友', 19);
console.log(ldh);
- 利用 new Object() 创建对象
var obj1 = new Object();
- 利用对象字面量创建对象
var obj2 = {};
# 八、深拷贝、浅拷贝
# 1. 深拷贝
深拷贝: 拷贝多层,每一级别的数据都会拷贝。
var obj = {
id: 1,
name: 'andy',
msg: {
age: 18
},
color: ['red', 'pink']
};
var o = {};
// 封装函数
function deepCopy(newobj, oldobj) {
for (var k in oldobj) {
// 判断属性值属于哪种数据类型
// 1. 获取属性值 oldobj[k]
var item = oldobj[k];
// 2. 判断这个值是否是数组
if (item instanceof Array) {
newobj[k] = [];
deepCopy(newobj[k], item)
// 3. 判断这个值是否是对象
} else if (item instanceof Object) {
newobj[k] = {};
deepCopy(newobj[k], item)
// 4. 是否属于简单数据类型
} else {
newobj[k] = item;
}
}
}
deepCopy(o, obj);
console.log(o);
var arr = [];
console.log(arr instanceof Object);
o.msg.age = 20;
console.log(obj);
# 2. 浅拷贝
浅拷贝: 只是拷贝一层,更深层次对象级别的只拷贝引用。
ES6 新增方法:
Object.assign(target, ...sources);
- target: 拷贝给谁
- sources: 拷贝的是谁
var obj = {
id: 1,
name: 'andy',
msg: {
age: 18
}
};
var o = {};
Object.assign(o, obj);
console.log(o);
o.msg.age = 20;
console.log(obj);
# 九、防抖、节流
# 1. 防抖(debounce)
在一定时间内,只执行最后一次任务。
在事件被触发的 N 秒后再执行任务(回调函数),如果在这 N 秒内又触发事件,则重新计时:
- 当事件触发时,相应的响应处理函数不会立即被触发,而是等待一定的时间;
- 当事件密集触发时,函数的触发会被频繁的推迟;
- 只有等待了一段时间也没有事件触发,才会真正执行响应处理函数。
防抖的应用场景:
- 手机号、邮箱输入检测
- 搜索框搜索输入(只需最后一次输入完后,再发送 Ajax 请求,减少请求次数,节约请求资源)
- 窗口大小 resize(只需窗口调整完成后,计算窗口大小,防止重复渲染)
- 滚动事件 scroll(只需执行触发的最后一次滚动事件的处理程序)
- 文本输入的验证(连续输入文字后发送 Ajax 请求进行验证,(停止输入后)验证一次就好
极简版:
const debounce = (func, wait)=>{
let time
return ()=>{
if(time) cleartTimeout(time)
time = setTimeout(func, wait)
}
}
带参数版本:
function debounce(func,wait){
var time
return ()=>{
// 如果定时器存在,需要清除定时器,重新定义
if(time) clearTimeout(time)
time = setTimeou(()={
func.apply(this,arguments)
time = null
},wait)
}
}
# 2. 节流(throttle)
在一定时间内,同一个任务只会执行一次。
在一定时间内,只能触发一次响应函数,如果事件频繁触发,只有一次生效。
节流的应用场景:
- DOM 元素的拖拽功能实现(mousemove)
- 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
- 计算鼠标移动的距离(mousemove)
- 搜索联想(keyup)
- 滚动事件 scroll,(只要页面滚动就会间隔一段时间判断一次)
极简版本:
const throttle = (func,wait)=>{
let time
return ()=>{
if(!time) time = setTimeout(func,wait)
}
}
带参数版本:
function throttle(func,wait){
let time
return ()=>{
if(!time){
time = setTimeout(()=>{
clearTimeout(time)
func.apply(this,arguments)
})
}
}
}
# 3. 防抖和节流的区别
防抖:某一段时间只执行一次(如果事件被频繁触发,防抖能保证只有最后一次触发生效!前面 n 多次的触发都会被忽略)。
节流:间隔时间执行(如果事件被频繁触发,节流能够减少事件触发的频率,因此节流是有选择地执行一部分事件)。
# 十、闭包
# 1.闭包(closure):
指有权访问另一个函数作用域中变量的函数。
function fn() {
var num = 10;
function fun() {
console.log(num);
}
fun();
}
fn();
# 2.闭包的特点:
- 函数嵌套函数。
- 函数内部可以引用外部的参数和变量。
- 参数和变量不会被垃圾回收机制回收。
# 3.闭包的优点:
- 变量长期驻扎在内存中
- 避免全局变量的污染
- 私有成员的存在
# 4.闭包的缺点:
函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏。
# 5.闭包的使用场景:
- 1.setTimeout:
原生的setTimeout传递的第一个函数不能带参数,通过闭包可以实现传参效果;
function f1(a) {
function f2() {
console.log(a);
}
return f2;
}
var fun = f1(1);
setTimeout(fun,1000); //一秒之后打印出1
2.回调:
定义行为,然后把它关联到某个用户事件上(点击或者按键)。代码通常会作为一个回调(事件触发时调用的函数)绑定到事件。3.防抖函数:
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。4.封装私有变量:
用js创建一个计数器:
function f1() {
var sum = 0;
var obj = {
inc:function () {
sum++;
return sum;
}
};
return obj;
}
let result = f1();
console.log(result.inc()); //1
console.log(result.inc()); //2
console.log(result.inc()); //3
# 十一、简述 MVVM
# 1. MVVM 的概念
视图模型双向绑定,是 Model-View-ViewModel 的缩写,也就是把 MVC 中的 Controller 演变成 ViewModel。
Model 层:代表数据模型。
View 层:代表 UI 组件。
ViewModel 层:是 View 和 Model 层的桥梁,数据会绑定到 ViewModel 层并自动将数据渲染到页面中,
视图(View)变化时会通知 ViewModel 更新数据。
以前是操作 DOM 结构更新视图,现在是数据驱动视图。
# 2. MVVM 的优点
- 低耦合:视图(View)可以独立于 Model 变化和修改,一个 Model 可以绑定到不同的 View 上, 当 View 变化时 Model 可以不变,当 Model 变化时 View 也可以不变;
- 可重用性:你可以把一些视图逻辑放在一个 Model 里面,让很多 View 重用这段视图逻辑。
- 独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
- 可测试。
# 十二、Promise
# 1.Promise
Promise 是解决回调地狱的构造函数。
例题:
const promise = new Promise((resolve, reject) => {
console.log(1)
resolve()
console.log(2)
})
promise.then(() => {
console.log(3)
})
console.log(4)
// 结果: 1 2 4 3
// promise 构造函数是同步执行的,then 方法是异步执行的
// Promise new 的时候会立即执行里面的代码
// then是微任务 会在本次任务执行完的时候执行,setTimeout是宏任务 会在下次任务执行的时候执行
# 2.promise 的三种状态:
- pending: 初始状态也叫等待状态
- resolved/fulfiled: 成功状态
- rejected: 失败状态
var p = new Promise(function(resolve,reject){
setTimeout(()=>{
let num = Math.random()
if(num > 0.5){
resolve();
}else{
reject();
}
},1000)
})
p.then(()=>{
console.log("大")
}).catch(()=>{
console.log("小")
})
状态一旦改变,就不会再变。创造 promise 实例后,它会立即执行。
# 3.Promise 的特点:
- Promise 对象的状态不受外界影响
- Promise 的状态一旦改变,就不会再变,任何时候都可以得到这个结果,状态不可以逆,
# 4.Promise 的缺点:
- 一旦新建它就会立即执行,无法中途取消
- 如果不设置回调函数,内部抛出的错误,不会反映到外部
- 当处于 pending(等待)状态时,无法得知目前的进展
# 5.Promise 解决的问题
- 回调地狱,代码难以维护,常常第一个的函数的输出是第二个函数的输入这种现象
- promise 可以支持多并发的请求,获取并发请求中的数据
这个 promise 可以解决异步的问题,本身不能说 promise 是异步的。
# 6.async await、Promise、setTimeout 的区别
- setTimeout:
console.log('script start') // 1. 打印 script start
setTimeout(function(){
console.log('settimeout') // 4. 打印 settimeout
}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end') // 3. 打印 script end
// 输出顺序:script start -> script end -> settimeout
- Promise:
Promise本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候,此时是异步操作,会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行,打印 p 的时候,是打印的返回结果,一个 Promise 实例。
console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
- async await:
async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 输出顺序:script start->async1 start->async2->script end->async1 end
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
# 十三、ES6 新特性,let、const
# 1. ES6 新特性
const 和 let、模板字符串、箭头函数、函数的参数默认值、对象和数组解构、
for...of 和 for...in、ES6 中的类。
# 2. var、let、const 的区别
ES6 之前创建变量用的是 var,ES6 之后创建变量用的是 let、const。
| 声明方式 | 变量提升 | 暂时性死区 | 声明、使用顺序 / 重复声明 | 块作用域有效 | 跨块、跨函数访问 |
|---|---|---|---|---|---|
| var | 会 | 不存在 | 允许先使用后声明 / 允许 | 不是 | 可以/不能 |
| let | 不会 | 存在 | 先声明后使用 / 不允许 | 是 | 不能 |
| const | 不会 | 存在 | 必须设置初始值,先声明后使用 / 不允许 | 是 | 不能 |
let 产生暂时性死区:
// 暂时性死区是浏览器的 bug:检测一个未被声明的变量类型时,不会报错,会返回 undefined
如:console.log(typeof a) // undefined
而:console.log(typeof a) // 未声明之前不能使用
let a
# 十四、面向对象及其特点
面向对象是一种思想,是基于面向过程而言的,就是说面向对象是将功能等通过对象来实现,将功能封装进对象之中,让对象去实现具体的细节;这种思想是将数据作为第一位,这是对数据一种优化,操作起来更加的方便,简化了过程。Js 本身是没有 class 类型的,但是每个函数都有一个 prototype 属性,prototype 指向一个对象,当函数作为构造函数时,prototype 就起到类似于 class 的作用。
面向对象有三个特点:
- 封装(隐藏对象的属性和实现细节,对外提供公 共访问方式)
- 继承(提高代码复用性,继承是多态的前提)
- 多态(是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象)
# 十五、Js 中常见的内存泄漏
1.意外的全局变量
2.被遗忘的计时器或回调函数
3.脱离 DOM 的引用
4.闭包
# 十六、JS 的语言特性
- 运行在客户端浏览器上;
- 不用预编译,直接解析执行代码;
- 是弱类型语言,较为灵活;
- 与操作系统无关,跨平台的语言;
- 脚本语言、解释性语言
# 十七、JS 的垃圾回收机制
# 1.垃圾回收机制
垃圾回收机制称 Garbage Collection 简称 GC。js 的垃圾回收机制就是定时回收闲置资源的一种机制,每隔一段时间,执行环境都会清理内存中一些没用的变量释放它所占用的内存。
核心思想:找到没用的变量,释放它们的内存。
# 2.回收策略
(1)标记清除:
标记清除是现在最常使用的垃圾回收策略, 使用标记清除作为垃圾回收机制的浏览器会在垃圾回收程序进行时会做如下几步 :
- 标记内存中所有的变量。
- 把在上下文(全局作用域,脚本作用域)中声明的变量,以及在全局被引用的变量的标记删除掉,剩下的所有带标记的变量就被视为要删除的变量,垃圾回收执行时释放它们占用的内存。
- 内存清理,清除垃圾。
实例:
// 变量 color,dog 在全局环境下声明,不会被清除
const color = 'red';
var dog = '金毛';
{
let cat = 'kitty'; // 变量 cat 在块作用域中声明,且没有被全局所引用,所以会在下一次垃圾回收执行时,释放其内存
}
(2)引用计数:
引用计数是一种不常用的垃圾回收策略,主要核心思路就是记录值被引用的次数,一个值被赋给变量,引用次数+1,这个变量在某个时刻重新赋了一个新值,旧值的引用次数 -1 变为了0,在下次垃圾回收程序进行时就会释放它的内存。
引用计数存在的问题:循环引用。
实例:
function fn() {
const obj1 = new Object()
// new Object 在堆内存中创建了一个对象1 {} 这个值被赋值给obj1 于是引用次数 + 1
const obj2 = new Object()
// new Object 在堆内存中创建了一个对象2 {} 这个值被赋值给obj2 于是引用次数 + 1
obj1.a = obj2; // obj2 被赋值给 obj1的a属性 于是对象1的引用次数 1+1 = 2
obj2.a = obj1; // obj1 被赋值给 obj2的a属性 于是对象2的引用次数 1+1 = 2
}
// 此时两个对象之间相互引用,如函数多次调用,又会重新执行多次函数体,又会多了n个相互引用的对象占用内存
obj1.a = null;
obj2.a = null;
// 通过设置为 null 可以切断两者之间的引用,在下次回收时就会清理释放掉
# 十八、JS 的 4 种设计模式
# 1. 发布者订阅模式
就例如我们关注了某个公众号,然后他对应的有新的消息就会给我们推送。 代码实现逻辑是用数组存贮订阅者,发布者回调函数里面通知的方式是遍历订阅者数组,并将发布者内容传入订阅者数组
//发布者与订阅模式:
var shoeObj = {}; // 定义发布者
shoeObj.list = []; // 缓存列表 存放订阅者回调函数
// 增加订阅者:
shoeObj.listen = function(fn) {
shoeObj.list.push(fn); // 订阅消息添加到缓存列表
}
// 发布消息:
shoeObj.trigger = function() {
for (var i = 0, fn; fn = this.list[i++];) {
fn.apply(this, arguments);//第一个参数只是改变fn的this,
}
}
// 小红订阅如下消息:
shoeObj.listen(function(color, size) {
console.log("颜色是:" + color);
console.log("尺码是:" + size);
});
// 小花订阅如下消息:
shoeObj.listen(function(color, size) {
console.log("再次打印颜色是:" + color);
console.log("再次打印尺码是:" + size);
});
shoeObj.trigger("红色", 40);
shoeObj.trigger("黑色", 42);
# 2. 工厂模式
简单的工厂模式可以理解为解决多个相似的问题。
# 3. 单例模式
只能被实例化(构造函数给实例添加属性与方法)一次。
# 4. 沙箱模式
将一些函数放到自执行函数里面,但要用闭包暴露接口,用变量接收暴露的接口,再调用里面的值,否则无法使用里面的值。
# 十九、同源策略、跨域
# 1. 同源策略:
是浏览器对 JavaScript 实施的安全限制,只要协议、域名、端口有任何一个不同,都被当作是不同的域。
# 2. 跨域:
是指浏览器不能执行其他网站的脚本,它是由浏览器的同源策略造成的。
# 3. 解决跨域问题:
- Jsonp实现跨域:
jsonp的原理:动态创建 script 标签,它的 src 属性是没有跨域限制的。
- 前端创建 script 标签,设置 src,添加到 head 中(你可以往 body 中添加)
- 后台返回一个 js 变量 jsonp,这个 jsonp 就是请求后的 JSON 数据
- 回调完成后删除 script 标签(还有一些清理工作如避免部分浏览器内存泄露等)
<script type="text/javascript">
function callback(data) {
alert(data.message);
}
function addScriptTag(src){
var script = document.createElement('script');
script.src = src;
document.body.appendChild(script);
}
window.onload = function(){
addScriptTag("http://www.foo.com/js/outer.js");
}
</script>
- 代理的方式:
服务器 A 的 test01.html 页面想访问服务器 B 的后台 action,返回“test”字符串,此时就出现跨域请求,浏览器控制台会出现报错提示,(由于跨域是浏览器的同源策略造成的,对于服务器后台不存在该问题),可以在服务器 A 中添加一个代理 action,在该 action 中完成对服务器 B 中 action 数据的请求,然后在返回到 test01.html 页面。
开发时:用 proxy(代理的意思)。
部署时:用 nginx。
- CORS
CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源(协议 + 域名 + 端口)服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。
浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
- 浏览器发出 CORS 简单请求:只需要在头信息之中增加一个 Origin 字段。
- 浏览器发出 CORS 非简单请求:会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。
btn.onclick = function(){
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300)|| xhr.status == 304){
result.innerHTML = xhr.responseText;
}else{
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("post", "https://www.webhuochai.com/test/iecors.php", true);
xhr.send(null);
}
# 4. img、iframe、script 来发送跨域请求的优缺点
iframe 优点:跨域完毕之后 DOM 操作和互相之间的 JavaScript 调用都是没有问题的。
缺点:
1.若结果要以 URL 参数传递,这就意味着在结果数据量很大的时候需要分割传递,巨烦。
2.还有一个是 iframe 本身带来的,母页面和 iframe 本身的交互本身就有安全性限制。script 优点:可以直接返回 json 格式的数据,方便处理。
缺点:只接受 GET 请求方式。图片 ping 优点:可以访问任何 url,一般用来进行点击追踪,做页面分析常用的方法。
缺点:不能访问响应文本,只能监听是否响应
# 二十、函数柯里化
简单来说,就是返回结果是函数的函数称为函数柯里化。
指的是将能够接收多个参数的函数转化为接收单一参数的函数,并且返回接收余下参数且返回结果的新函数的技术。
函数柯里化的主要作用和特点就是参数复用、提前返回和延迟执行。
在一个函数中,首先填充几个参数,然后再返回一个新的函数的技术,称为函数的柯里化。
通常可用于在不侵入函数的前提下,为函数预置通用参数,供多次重复调用。
const add = function add(x) {
return function (y) {
return x + y
}
}
const add1 = add(1)
add1(2) === 3
add1(20) === 21
# 二十一、事件循环 (Event loop)
# 1. 执行过程:
同步任务和异步任务分别进入不同的执行"场所";
同步任务进入主线程形成一个执行栈(execution context stack)
异步任务进入 注册站(Event Table)并注册回调函数。
当指定的事情完成 (如 Ajax 请求响应返回,setTimeout 延迟到指定时间) 时,注册站(Event Table)会将这个函数移入任务队列(task quene),等待主线程的任务执行完毕;
当栈中的代码执行完毕,任务为空时,主线程会先检查micro-task(微任务)队列中是否有任务,如果有,就将micro-task(微任务)队列中的所有任务依次执行,直到micro-task(微任务)队列为空;
之后再检查macro-task(宏任务)队列中是否有任务,如果有,则取出第一个macro-task(宏任务)加入到执行栈中,之后再清空执行栈,检查micro-task(微任务),以此循环,直到全部的任务都执行完成。
如此循环,就形成 js 的事件循环机制(Event Loop)。

例题:
// 现在我有一个网络请求,需要从服务器获取一个数据:
console.log('1');
// 向 http://example.com 网址发送了一个网络请求
axios('http://example.com')
.then( res =>{
// res 是服务器返回的内容
console.log(res);
})
console.log('3');
// 如果没有事件循环,分析一下上面的代码的执行结果:
// 首先执行第一行,打印 1,再执行第二个函数(axios),
// 并等待结果,服务器给我们返回结果以后,
// 打印返回的结果 res,最后打印 3(浏览器不是这样执行的)。
// 如果服务器访问速度很慢,执行到第二行,浏览器会处于一直等待状态,
// 这时候用户点击一个网页上的点击按钮,这个操作等到服务器返回数据之后执行。
// 这种用户体验是很差,所以就有了 javascript 的事件循环机制。
// 打印顺序是:1、3、res
# 2. 组成部分:
- 执行栈(Call Stack)
- 消息队列 (Message Queue)
# (1) 执行栈(Call Stack)
普通函数执行时先放入调用栈中按顺序执行并立即释放。
function f2(){
console.log("22")
}
function f1() {
console.log("11");
f2()
console.log("33")
}
f1()
// 打印顺序是:11 22 33
# (2) 消息队列(Message Queue)又称任务队列:
事件放入消息队列中,任务队列分为: macro-task(宏任务)队列,micro-task(微任务)队列,执行完执行栈中的任务后执行消息队列中的任务。

| 宏任务(macro-task) | 微任务(micro-task) |
|---|---|
| script(整体代码) | process.nextTick |
| setTimeout | Promise |
| setInterval | Async/Await(实际就是 promise) |
| setImmediate | MutationObserver(html5 新特性) |
| UI render | |
| I/O (比如 Ajax 操作从网络读取数据) |
再看下面的例子:
console.log("00")
setTimeout(() => {
console.log("22")
}, 0)
console.log("11")
// 分析一下上面的代码的执行结果:
// 首先遇到普通函数立即执行,所以打印 “00”。
// 遇到 setTimeout 放入消息队列中,往下执行。
// 然后打印 “11”,清空执行栈。
// 最后运行消息队列中的任务,打印“22”。
// 打印顺序是:00 11 22
# 二十二、回流和重绘
# 1. 回流和重绘
(1)回流(Reflow):(又称重排)引起 DOM 树结构变化,改变页面布局的过程。
(2)重绘(Repaint):不影响页面布局,只跟样式有关的元素进行重新绘制的过程。
! 注意:回流必引起重绘,而重绘不一定会引起回流。
# 2. 减少回流和重绘的方法
(1)CSS 中避免回流和重绘:
尽量在 DOM 树的最末端改变 class 避免设置多层内联样式 动画效果应用到 position 设置为 absolute 或者 fixed 的元素上 避免使用 table 布局 使用 CSS3 硬件加速,可以让 transform、opacity、filters 等动画不会引起重绘
(2)JS 避免回流和重绘:
避免使用 JS 一个样式修改完接着下一个样式,最好一次性更改 CSS 样式,或者将样式定义好 class 避免频繁操作 DOM,使用文档片段创建子树,然后再拷贝到文档中 先隐藏元素,进行修改后再显示该元素,因为 display:none 上的DOM操作不会引起回流和重绘 避免循环读取 offsetleft 等属性,在循环之前把他们存起来 对于复杂的动画效果,使用绝对定位让其脱离文档流,否则会引起父元素及后续元素大量回流
# 3. Vue 的 v-if 与 v-show 区别
(1)v-if:是通过控制 DOM 节点的存在与否来控制元素的显隐。
(2)v-show:是通过设置 DOM 元素的 display 样式,block为显示,none 为隐藏。
如需要频繁切换建议使用 v-show,如在运行时条件很少改变,则用 v-if。
# 二十三、事件流
事件流:事件流描述的是从页面中接收事件的顺序,DOM2 级事件流包括下面几个阶段:
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
addEventListener:addEventListener 是 DOM2 级事件新增的指定事件处理程序的操作,
这个方法接收 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。
最后这个布尔值参数如果是 true,表示在捕获阶段调用事件处理程序;
如果是 false,表示在冒泡阶段调用事件处理程序。
IE 只支持事件冒泡。
# 二十四、事件委托、事件源
事件委托:不在事件的发生地(直接 dom)上设置监听函数,而是在其父元素上设置监听函数,
通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素 DOM 的类型,
来做出不同的响应。
举例:最经典的就是 ul 和 li 标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在 li 标签上直接添加,而是在 ul 父元素上添加。
好处:比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制。
Event.target 谁调用谁就是事件源。
# 二十五、事件冒泡
一个事件触发后,会在子元素和父元素之间传播,这种传播分为三个阶段:
- 捕获阶段:(从 window 对象传导到目标节点(从外到里),这个阶段不会响应任何事件)。
- 目标阶段:(在目标节点上触发)。
- 冒泡阶段:(从目标节点传导回 window 对象(从里到外))。
事件委托/事件代理就是利用事件冒泡的机制把里层需要响应的事件绑定到外层。
# 二十六、Cookie、sessionStorage、localStorage 的区别
共同点:都是保存在浏览器端。
不同点:
localStorage:它不会自动把数据发给服务器,仅在本地保存,它在所有同源窗口中都是共享的。
(key:即使窗口或者浏览器关闭与否都会始终生效,因此用作持久保存数据)Cookie:它始终在 http 请求中自身携带(即使不需要),它在浏览器和服务器间来回传递。它在所有同源窗口中都是共享的。
(key:即使窗口或浏览器关闭,设置的 cookie 时间过期之前一直有效,存储容量只有 4K 左右)sessionStorage:它不会自动把数据发给服务器,仅在本地保存。
(key:在当前浏览器窗口关闭前有效,自然也就不可能持久保存)