# 1. JS 的数据类型

6 种简单数据类型undefinednullbooleannumberstringsymbol(ES6) (表示独一无二的值,可以保证属性不重名)
1 种复杂的数据类型Object

栈(stack):原始数据类型StringBooleanNumberNullUndefined占据空间小、大小固定 堆(heap):引用数据类型Object(Array, Date, RegExp, Function)占据空间大、大小不固定

# Boolean、Number、String 这三个是 Javascript 中的基本包装类型,也就是这三个其实是一个构造函数,他们是 Function 的实例,是引用类型

WARNING

需要注意的是 typeof null 返回为 object,因为特殊值 null 被认为是一个空的对象引用。undefined 值是派生自 null 值的,因此 ECMA-262 规定对它们的相等性测试要返回 true

console.log(undefined == null); //true
1

# 2. tpyeof 可以返回多少种值

let a = "d"; // string
let num = 3232; // number
let str = new String("dd"); // object
let un = undefined; // undefined
let bool = true; // boolean
function aa() {} // function
Symbol("sym"); // symbol (ES6)
1
2
3
4
5
6
7

# 3. JS 中 new 一个对象会发生什么

当执行 const obj = new MyClass() 时,会发生以下过程:

  • 3.1 创建一个空对象
const obj = {}; // 或更底层:obj = Object.create(MyClass.prototype);
1
  • 3.2 绑定原型(Prototype Link)

将新对象的 __proto__(隐式原型) 指向构造函数的 prototype(显式原型) 属性

obj.__proto__ = MyClass.prototype;
1
  • 3.3 执行构造函数(绑定 this)

调用构造函数 MyClass,并将 this 指向新创建的对象:

MyClass.call(obj); // 构造函数内部的 this 就是 obj
1
  • 3.4 处理返回值

如果构造函数 返回一个对象(如 return { foo: 1 }),则 new 的结果是这个对象。

如果返回 非对象值(如 return 1),则忽略返回值,仍然返回新创建的对象 obj

# 手动实现的 new 操作符逻辑

function myNew(constructor, ...args) {
  // 1. 创建空对象并绑定原型
  const obj = Object.create(constructor.prototype);

  // 2. 执行构造函数(绑定 this)
  const result = constructor.apply(obj, args);

  // 3. 处理返回值
  return result instanceof Object ? result : obj;
}

// 使用示例
const obj = myNew(MyClass, arg1, arg2);
1
2
3
4
5
6
7
8
9
10
11
12
13

WARNING

箭头函数不能作为构造函数

箭头函数没有 this 绑定和 prototype 属性,因此不能 new:

const Foo = () => {};
new Foo(); // TypeError: Foo is not a constructor
1
2

# 4. 预解析

预解析是 JavaScript 引擎在执行代码前的一个预处理阶段,它会将变量和函数声明提升到当前作用域的顶部。

# 4.1 预解析的基本表现

  • 变量声明提升(var)
console.log(a); // 输出: undefined
var a = 5;
console.log(a); // 输出: 5
1
2
3

实际执行顺序相当于:

var a;          // 声明提升到作用域顶部
console.log(a); // 此时a已声明但未赋值
a = 5;          // 赋值保留在原位置
console.log(a);
1
2
3
4
  • 函数声明提升
foo(); // 输出: "Hello"

function foo() {
  console.log("Hello");
}
1
2
3
4
5

函数声明整体被提升到作用域顶部。

# 4.2 不同声明方式的预解析差异

声明方式 提升表现 示例
var 声明提升,值为undefined console.log(x); var x = 5;
function 整个函数体提升 foo(); function foo() {}
let/const 提升但存在"暂时性死区" console.log(y); let y = 5; → 报错
函数表达式 按变量提升规则处理 bar(); var bar = function() {} → 报错

# 4.3 预解析的优先级规则

函数声明优先于变量声明

console.log(typeof foo); // 输出: "function"

var foo;
function foo() {}
1
2
3
4

后面的函数声明会覆盖前面的

foo(); // 输出: "Second"

function foo() { console.log("First"); }
function foo() { console.log("Second"); }
1
2
3
4

# 4.4 let/const 的暂时性死区(TDZ)

虽然 letconst 也会被提升,但在声明前访问会报错:

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
1
2

# 4.5 实际开发中的注意事项

    1. 避免先使用后声明:虽然预解析允许,但会降低代码可读性
    1. 使用函数表达式时注意:
// 这样会报错
myFunc(); 
var myFunc = function() {};

// 正确方式
var myFunc = function() {};
myFunc();
1
2
3
4
5
6
7
    1. 推荐使用 let/const:避免 var 的提升问题

# 5. call、apply 和 bind 的理解

# call、apply 和 bind 是 Function 对象自带的三个方法,属于对象冒充,可以调用另外一个对象的方法,同时改变函数体内部 this 的指向。

方法 调用时机 参数传递方式 返回值
call 立即调用 单独传递 函数执行结果
apply 立即调用 数组形式传递 函数执行结果
bind 不立即调用 单独传递或部分传递 绑定后的新函数

# 自己动手实现 call 方法

Function.prototype.myCall = function(context, ...arg) {
  let obj = context || window;
  obj.fn = this;
  let result = obj.fn(...arg);
  delete obj.fn;
  return result;
};
1
2
3
4
5
6
7

# 自己动手实现 apply 方法

Function.prototype.myApply = function(context, arg) {
  let obj = context || window;
  obj.fn = this;
  let res;
  if (arg) {
    res = obj.fn(...arg);
  } else {
    res = obj.fn;
  }
  delete obj.fn;
  return res;
};
1
2
3
4
5
6
7
8
9
10
11
12

# 实际应用场景

  • 1.借用方法:使用其他对象的方法
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };

function showName() {
  console.log(this.name);
}

showName.call(obj1); // "obj1"
showName.call(obj2); // "obj2"
1
2
3
4
5
6
7
8
9
  • 2.类数组转为数组
function list() {
  return Array.prototype.slice.call(arguments);
}
1
2
3
  • 3.延迟执行(bind)
const button = document.querySelector('button');
button.addEventListener('click', this.handleClick.bind(this));
1
2

# 6. 事件绑定 3 种方法

1、HTML 事件处理(在 dom 元素中嵌入)

<button onclick="fn()"></button>
1

WARNING

  • 缺点:
    1、this 指向 window
    2、HTML 与 JS 紧密耦合,改动代码麻烦

2、DOM0 级事件处理(获取 dom 元素直接绑定)

document.getElementById("btn").onclick = fn;
1

WARNING

  • 优点:
    1、this 指向 dom 元素
    2、不存在浏览器兼容问题

3、DOM2 级事件处理(事件监听)

document.getElementById("btn").addEventListener("click", fn);
1

WARNING

  • 优点:
    1、this 指向 dom 元素
  • 缺点:
    1、需要对 IE8 及以下进行兼容

TIP

tip: document.getElementById('btn').attachEvent('click',fn)
由于 IE8 及以下只支持事件冒泡,所以通过 attachEvent 都会被添加到冒泡阶段
IE 中的 attachEvent 中的 this 总是指向全局对象 Window

# 7. 事件冒泡、捕获和阻止

# 事件冒泡:即事件开始时由最具体的元素(文档中嵌套层数最深的那个点)接收,然后逐级向上传播到较为不具体的节点(文档)

由 div -> body -> html -> document

# 事件捕获:不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件

由 document -> html -> body -> div

因为老版本的不支持,因此很少会用事件捕获,多数在事件冒泡下处理程序

DOM2 级规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段

# 阻止事件冒泡:event.stopPropagation()

一个阻止事件冒泡的办法就是使用 event.stopPropagation()
在 IE<9 的浏览器上使用 event.cancelBubble = true

# 取消事件的默认行为:preventDefault()

1、在其他浏览器中调用方法 e.preventDefault()
2、在 IE 浏览器中通过 e.returnValue = false

# 当 return false“的时候

在 DOM0 级事件中,可以像 event.preventDefault() 取消默认事件,但是在 DOM2 级则不行

# stopImmediatePropagation()

这个方法会停止一个事件继续执行,即使当前的对象上还绑定了其它处理函数
所有绑定在一个对象上的事件会按绑定顺序执行

# 8. 事件委托

也叫事件代理

  • 好处: 节省内存:每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大
    不需要为每个子节点注销事件
    不知道子节点的具体数量时(下拉加载图片)
  • 原理:
    事件委托是利用事件的冒泡原理来实现的
    通过 e.target 获取具体子元素

# 9. dom 事件中 currentTarget 和 target 的区别

  • 本质区别是:
    event.target 返回触发事件的元素
    event.currentTarget 返回绑定事件的元素
<!DOCTYPE html>
<html>
  <body>
    <div style="width: 30px;height: 30px;background-color: #0e91e1"></div>
  </body>
  <script>
    document.querySelector("body").onclick = function(e) {
      console.log("currentTarget:", e.currentTarget);
      console.log("target:", e.target);
    };
  </script>
</html>

点击div时: currentTarget: body target: div 点击body时: currentTarget: body
target: body
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 10. script 中 defer 和 async 的区别

参考: (opens new window)

script:会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。
async script:解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。
defer script:完全不会阻碍 HTML 的解析,解析完成之后再按照顺序执行脚本。

  • defer 和 async 都是只适用于外部的脚本文件,都是用来告诉浏览器立即下载文件,但是延迟执行
  • 这些脚本文件会等到 body 的内容执行完才执行
    在 HTML5 的规范中,defer 属性要求脚本是按照出现的先后顺序执行,但是在现实中,defer 属性的脚本不一定会按照顺序执行
    而 async 属性的脚本执行的顺序并不确定
<script async src="1.js"></script> <!-- 可能最后执行 -->
<script defer src="2.js"></script> <!-- 保证第二个执行 -->
<script src="3.js"></script>      <!-- 最先执行 -->
<script defer src="4.js"></script> <!-- 保证最后执行 -->
1
2
3
4

# 11. this 指向

  • 在普通函数中,this 的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定 this 到底指向谁,实际上 this 的最终指向的是那个调用它的对象
  • 在箭头函数中,this 的指向是定义时所在的对象,而不是使用时所在的对象

# 12. 事件执行机制

  • javascript 是一门单线程语言
    JS 在执行的过程中会产生执行环境,这些执行环境会被按照顺序的加入到执行栈中
    同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的代码,会被挂起并加入到 Task(有多种 task) 队列中

  • 除了广义的同步任务和异步任务,还包括有更加精确的微任务和宏任务
    微任务包括 process.nextTick,promise,Object.observe,MutationObserver
    宏任务包括 script,setTimeout,setInterval,setImmediate,I/O,UI rendering

  • 为什么需要微任务:是为了给紧急任务一个插队的机会

当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。

  • 所以正确的一次 Event loop 顺序是这样的
    1.执行同步代码,这属于宏任务
    2.执行栈为空,查询是否有微任务需要执行
    3.执行所有微任务
    4.必要的话渲染 UI
    5.然后开始下一轮 Event loop,执行宏任务中的异步代码
console.log('a');
 
new Promise(resolve => {
    console.log('b')
    resolve()
}).then(() => {
    console.log('c')
    setTimeout(() => {
      console.log('d')
    }, 0)
})
 
setTimeout(() => {
    console.log('e')
    new Promise(resolve => {
        console.log('f')
        resolve()
    }).then(() => {
        console.log('g')
    })
}, 100)
 
setTimeout(() => {
    console.log('h')
    new Promise(resolve => {
        resolve()
    }).then(() => {
        console.log('i')
    })
    console.log('j')
}, 0)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  • 上述代码执行结果:a b c h j i d e f g

1.打印a
2.promise立即执行,打印b
3.promise.then推入微任务队列
4.setTimeout推入宏任务队列
5.整段代码执行完毕,开始执行微任务,打印c,遇到setTimeout推入宏任务队列排队等待执行
6.没有可执行的微任务开始执行宏任务,定时器按照延迟时间排队执行
7.打印h j,promise.then推入微任务队列有
8.可执行的微任务,打印i,继续执行宏任务,打印d
9.执行延迟为100的宏任务,打印e f,执行微任务打印g,所有任务执行完毕

# 13. jS 继承方式总结

参考 (opens new window)

一共6种,构造函数、原型链继承、组合继承、寄生式继承、寄生式组合继承、ES6继承

  • 借用构造函数继承
function Parent() {}
function Children() {
  Parent.call(this); // 或apply
}
1
2
3
4

缺点:父类原型上的东西是没法继承的,因此函数复用也就无从谈起

  • 原型链继承
function Parent() {}
function Children() {}
Children.prototype = new Parent();
1
2
3

这种方式确实解决了上面借用构造函数继承方式的缺点
但是,这种方式仍有缺点:实例化了两个 Child 原型链上中的原型对象它俩是共用的

  • 组合继承
function Parent() {}
function Children() {}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;
1
2
3
4
  • 寄生式继承
function createAnother(original){
    var clone = object(original); //通过调用函数创建一个新对象
    clone.sayHi = function(){  // 某种方式增强这个对象
        console.log("hi");
    }
    return clone;  // 返回这个对象
}

var person = { name: "james"}
var anotherPerson = createAnother(person);

anotherPerson.sayHi(); // "hi"
1
2
3
4
5
6
7
8
9
10
11
12

优点:在主要考虑对象而不是自定义类型和构造函数的情况下,实现简单的继承。
缺点:使用该继承方式,在为对象添加函数的时候,没有办法做到函数的复用。

  • ES6 继承

Class 可以通过 extends 关键字实现继承

# 14.原型和原型链

# prototype 属性:

这是一个显式原型属性,只有函数才拥有该属性。

# proto 属性:

这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。

# 什么是原型链:

  • 原型链:实例与原型之间的链接

由于 proto 是任何对象都有的属性,而 js 里万物都是对象,所以会形成一条proto连起来的链条,递归访问proto必须最终到头,并且值是null

在寻找对象方法的时候,会先寻找自身的方法,如果没有,就会通过proto去寻找该构造函数原型上的方法,如果没有,就会进一步寻找 Object原型上的方法,直到最后指向null。像这样通过proto连起来的链条寻找 prototype 就叫原型链

function Person(){}
var p = new Person()
p.__proto__ === Person.prototype // true
1
2
3

# js 获取原型的方法

p.__proto__
p.constructor.prototype
Object.getPrototypeOf(p)
1
2
3

原型和原型链

# 15. 闭包

  • 1、概念:有权访问另一个函数作用域和变量的函数,创建闭包最简单的方式就是在一个函数内部创建另一个函数。
function outer(){
    var a = 1;
    function inner(){
        console.log(a);
    }
    inner(); // 1
}
outer();
1
2
3
4
5
6
7
8
  • 2、好处:由于可以读取函数内部的变量,如果希望一个变量常驻于内存中又可全局访问,同时又想避免全局变量的污染,此时使用闭包就是一种恰当的方式
  • 3、缺点:但正是因为函数内部变量被外部所引用,不会被垃圾回收,就会造成常驻内存,使用过多容易造成内存泄漏

# 16. 节流和防抖

  • 防抖和节流的作用都是防止函数多次调用 区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于 wait,防抖的情况下只会调用一次(你越点我,我就越不执行)
    而节流的 情况会每隔一定时间(参数 wait)调用函数。

# 手写防抖

function debounce(fn, wait = 1000) {
  let timeout = 0;
  return function(...args) {
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => {
      fn.apply(this, args);
    }, wait);
  };
}
1
2
3
4
5
6
7
8
9
10
11

# 手写节流

function throttle(fn, wait = 1000) {
  let timeout = 0;
  return function() {
    if (timeout) {
      timeout = setTimeout(() => {
        fn();
        timeout = 0;
      }, wait);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11

# 17. setTimeout 和 setInterval 的运行机制

setTimeout() 和 setInterval() 都是属于定时器,同属于 window 方法,所以 this 会指向 window,它们的区别在于 setTimeout 只调用一次,而 setInterval 是重复调用。它们两者都是属于异步机制,属于宏任务,只是在指定的时间里将要执行的内容放到任务队列里。

  • 使用 setInterval 会出现两个问题
    (1) 某些时间间隔会被跳过
    (2) 多个定时器代码的执行之间的间隔可能会比预期的小

# ​​用 setTimeout 实现 setInterval 的功能​

代码实现

function mySetInterval(callback, delay) {
  let timerId;

  // 定义递归函数
  const execute = () => {
    callback();          // 执行回调
    timerId = setTimeout(execute, delay); // 递归调用
  };

  // 第一次启动
  timerId = setTimeout(execute, delay);

  // 返回一个清除定时器的方法
  return {
    clear: () => {
      clearTimeout(timerId);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

使用示例

// 定义一个回调函数
function sayHello() {
  console.log("Hello!");
}

// 使用自定义的 mySetInterval
const interval = mySetInterval(sayHello, 1000);

// 5秒后停止
setTimeout(() => {
  interval.clear();
  console.log("Interval stopped!");
}, 5000);
1
2
3
4
5
6
7
8
9
10
11
12
13

# 对比 setInterval 和 setTimeout 实现​

​​特性 setInterval setTimeout 递归实现
​​执行机制​ 固定间隔执行,可能堆积 前一次执行完才安排下一次
​​误差累积​ 可能累积延迟(如回调执行时间较长) 无累积误差
​​可控性​ 只能 clearInterval 可动态调整 delay 或停止
​​内存泄漏风险​ 不清理会导致持续执行 可手动清理

# 18. JS循环

  • 1、最早遍历数组的方法:for
for (var i = 0; i < arr.length; i++) {}
1
  • 2、ES5 的 forEach 遍历数组
arr.forEach(function(item, index) {});
1

WARNING

但是你不能使用 break 和 return 来退出循环

  • 3、ES5 的 for-in 循环
for (let key in arr) {
  console.log(key); //string
  console.log(arr[key]);
}
1
2
3
4
  • 4、ES6 的 for-of 循环
for (let key of arr) {
  console.log(value);
}
1
2
3

for in 是 ES5 标准,遍历 key,for of 是 ES6 标准,遍历 value
推荐在循环对象属性的时候,使用 for...in,在遍历数组的时候的时候使用 for...of for...of 不能循环普通的对象,需要通过和 Object.keys()搭配使用

  • for in 遍历数组是一个糟糕的选择
    1.index 索引为字符串型数字,不能直接进行几何运算
    2.遍历顺序有可能不是按照实际数组的内部顺序
    3.使用 for in 会遍历数组所有的可枚举属性,包括原型。例如上栗的原型方法 method 和 name 属性。 所以 for in 更适合遍历对象,不要使用 for in 遍历数组
    4.for-in 这个代码是为普通对象设计的,不适用于数组的遍历

  • for of 用于遍历数组
    for..of 适用遍历数/数组对象/字符串/map/set 等拥有迭代器对象的集合.但是不能遍历对象,因为没有迭代器对象.与 forEach()不同的是,它可以正确响应 break、continue 和 return 语句
    for-of 循环不支持普通对象,但如果你想迭代一个对象的属性,你可以用 for-in 循环(这也是它的本职工作)或内建的 Object.keys()方法:

# 19. 跨域是什么,怎么解决

跨域指的是浏览器出于安全考虑的同源策略,当从一个地址向另外一个地址请求资源时,协议、域名、端口号任意一个不同都属于跨域

  • 什么是同源策略 同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。

  • 解决方法

# 1、 JSONP(JSON With Padding):

JSONP 实现跨域请求的原理简单的说,就是利用<script>的 src 不受同源策略约束来跨域获取数据
jsonp 只支持 get 请求而不支持 post 请求
JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。
1
2
3

# 2、CORS (全称是"跨域资源共享"(Cross-origin resource sharing)):

参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS CORS 原理:使用自定义的 HTTP 头部让浏览器和服务器沟通
如添加一个额外的 Origin 头部,包含请求页面的的地址信息(协议、域名、端口号)
在后台设置 Access-Control-Allow-Origin 即可

# CORS 预检请求:

使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。
"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

# 简单请求:

某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”

  • 满足简单请求的条件:
    1、请求为 GET、HEAD、POST 其一
    2、请求字段满足 CORS 安全集合的字段
    3、Content-Type 有限制
// 允许访问源
header('Access-Control-Allow-Origin:\*');
// 允许访问的有效期
header('Access-Control-Max-Age:86400');
// 允许访问的方法
header('Access-Control-Allow-Methods:OPTIONS, GET, POST, DELETE');
1
2
3
4
5
6
  • 其他跨域: document.domain + iframe 跨域
    window.name + iframe 跨域
    html5 的 postMessage
    img 的 src 属性、link 的 href 属性、script 的 src 属性
    websocket

# 20. 关于 JS 的堆栈和拷贝

# 1、栈(stack)和堆(heap)

stack 为自动分配的内存空间,它由系统自动释放
而 heap 则是动态分配的内存,大小不定也不会自动释放

# 2、基本类型和引用类型

基本类型:存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配
5 种基本数据类型有 Undefined、Null、Boolean、Number 和 String,它们是直接按值存放的,所以可以直接访问。
引用类型:存放在堆内存中的对象,变量实际保存的是一个指针,这个指针指向另一个位置。每个空间大小不一样,要根据情况开进行特定的分配。
当我们需要访问引用类型(如对象,数组,函数等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据

# 3、浅拷贝

前面已经提到,在定义一个对象或数组时,变量存放的往往只是一个地址。当我们使用对象拷贝时,如果属性是对象或数组时,这时候我们传递的也只是一个地址
因此子对象在访问该属性时,会根据地址回溯到父对象指向的堆内存中,即父子对象发生了关联,两者的属性值会指向同一内存空间

# 4、深拷贝

我们不希望父子对象之间产生关联,那么这时候可以用到深拷贝
既然属性值类型是数组和或象时只会传址,那么我们就用递归来解决这个问题,把父对象中所有属于对象的属性类型都遍历赋给子对象即可

# 实现深拷贝:

JSON.parse() 方法用于将一个 JSON 字符串转换为对象
JSON.stringify() 方法用于将 JavaScript 值(通常为对象或数组)转换为 JSON 字符串 该方法也是有局限性的: 会忽略 undefined
会忽略 symbol
不能序列化函数
不能解决循环引用的对象 -> 报错:Converting circular structure to JSON *可以用 lodash 来深拷贝函数

ES6:Object.assign()和展开运算符(...)对于深浅拷贝的结果是一样
如果拷贝的层数超过了一层的话,那么就会进行浅拷贝

# 21. 内存泄漏是什么

本质上,内存泄漏可以定义为一个应用,由于某些原因不再需要的内存没有被操作系统或者空闲内存池回收

# 1、意外的全局变量

  • 在一个函数你忘记用变量声明符(var 或 let)来声明的变量,一个意外的全局变量就被创建了
function a() {
  str = "str";
}
console.log(window.str);
1
2
3
4
  • 在函数中通过 this 赋予变量,在函数中,this 指向 window
function a() {
  this.str = "str";
}
console.log(window.str);
1
2
3
4

为了阻止这种错误发生,在你的 Javascript 文件最前面添加'use strict;' 这开启了解析 JavaScript 的阻止意外全局的更严格的模式
如果必须用全局变量来存储很多数据,在处理完之后,确保对其清零或重新赋值

# 2、定时器 setTimeout setInterval 以及回调函数

当不需要 setInterval 或者 setTimeout 时,定时器没有被 clear,定时器的回调函数以及内部依赖的变量都不能被回收,造成内存泄漏
比如:vue 使用了定时器,需要在 beforeDestroy 中做对应销毁处理。js 也是一样的

# 3、闭包(在全局作用域上保留着闭包局部变量的引用)

# 4、移除存在绑定事件的 DOM 元素(IE8 及以下)

# 5、循环引用

# 22. WEB 安全

# 1、XSS:跨站脚本(Cross-site scripting)

利用恶意脚本,攻击者可获取用户的敏感信息如 Cookie 等
根据攻击的来源,XSS 攻击可分为反射型、存储型和 DOM 型三种。

# 1、反射型

反射型 XSS 只是简单地把用户输入的数据反射给浏览器。往往需要引诱用户点击一个链接,才能攻击成功

# 2、存储型

存储型会把用户输入的数据“存储”在服务器端。比如说评论功能

  • XSS 的防御措施:
    1、使用 cookie 的 httpOnly 属性,加上了这个属性的 cookie 字段,js 是无法进行读写的
    2、对输入输出进行过滤转义
    3、内容安全策略 (CSP) 通过 HTTP Header 中的 Content-Security-Policy 来开启 CSP

# 2、CSRF:跨站请求伪造(Cross-site request forgery)

CSRF 的攻击主要是在用户不知情的情况下,冒充用户偷偷发了请求

  • 要完成一次 CSRF 攻击,受害者必须依次完成两个步骤:
    1、登录受信任网站 A,并在本地生成 Cookie
    2、在不登出 A 的情况下,访问危险网站 B。

  • CSRF 防御措施:
    1、检测 http referer 是否是同域名
    2、避免登录的 session 长时间存储在客户端中
    3、关键请求使用验证码或者 token 机制

# 23. JS创建对象的几种方式

参考 (opens new window)
一共7种,包含工厂模式、构造函数模式、原型模式、组合使用构造函数模式和原型模式、动态原型模式、寄生构造函数模式、稳妥构造函数模式

# 工厂模式

function createPerson(name){
    var o = new Object();
    o.name = name;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}

var person1 = createPerson("james");
var person2 = createPerson("kobe");
1
2
3
4
5
6
7
8
9
10
11

优点:解决了创建多个相似对象时,代码的复用问题
缺点:使用工厂模式创建的对象,没有解决对象识别的问题(就是怎样知道一个对象的类型是什么)

# 构造函数模式

function createPerson(name){
    this.name = name;
    this.sayName = function(){
        alert(this.name);
    };
    return o;
}

var person1 = new createPerson("james");
var person2 = new createPerson("kobe");
1
2
3
4
5
6
7
8
9
10

优点:解决了工厂模式中对象类型无法识别的问题,并且创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
缺点:我们知道 ECMAScript 中的函数是对象,在使用构造函数创建对象时,每个方法都会在实例对象中重新创建一遍。拿上面的例子举例,这意味着每创建一个对象,我们就会创建一个 sayName 函数的实例,但它们其实做的都是同样的工作,因此这样便会造成内存的浪费。

# 原型模式

function Person(){}

Person.prototype.name = "james";
Person.prototype.sayName = function(){
    alert(this.name);
}

var person1 = new Person();
person1.sayName(); // "james"

var person2 = new Person();
person2.sayName(); // "james"

console.log(person1.sayName === person2.sayName) // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14

优点:解决了构造函数模式中多次创建相同函数对象的问题,所有的实例可以共享同一组属性和函数。 缺点:原型模式省略了构造函数模式传递初始化参数的过程,所有的实例在默认情况下都会取得默认的属性值,会在一定程度上造成不方便。

# 24. 作用域链

参考 (opens new window)

定义:当代码在一个环境中执行时,会创建变量对象的一个作用域链。
作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。

# 创建过程

  • 全局上下文阶段,创建全局对象。
  • 将全局对象压入作用域链
  • 为全局对象中所有函数创建[[Scope]]属性,并将作用域链保存到该属性。(若无函数则跳过此步骤)
  • 每一个函数上下文阶段,复制函数的[[Scope]]属性,创建作用域链
  • 创建活动对象,并用 arguments 创建活动对象
  • 将活动对象压入当前上下文中的作用域链
  • 为活动对象中所有函数创建[[Scope]]属性,并将作用域链保存到该属性。(若无函数则跳过此步骤)

# 25. JS 延迟加载的方式

JS 延迟加载,也就是等页面加载完成之后再加载JavaScript文件。 JS 延迟加载有助于提高页面加载速度

  • defer 属性
  • async 属性
  • 动态创建 DOM 方式
  • 使用 setTimeout 延迟方法
  • 让 JS 最后加载(对文档的加载事件进行监听,当文档加 载完成后再动态的创建 script 标签来引入 js 脚本)

# 26. JS 模块化方案

  • 4种方案:CommonJS、AMD、CMD、ES6

# 26-1、CommonJS

通过 require 来引入模块,通过 module.exports 定义模块的 输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在 服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在 浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。

# 26-2、AMD

采用异步加载的方式来加载模块,模块的加载不影响后面语句的 执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。 require.js 实现了 AMD 规范。

// AMD 默认推荐 
define(["./a", "./b"], function(a, b) { 
  // 依赖必须一开始就写好
   a.doSomething(); 
   // 此处略去 100 行 
   b.doSomething(); 
   // ...
});
1
2
3
4
5
6
7
8

# 26-3、CMD

这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现 了 CMD 规范。它和 require.js 的区别在于模块定义时对依赖的处理不同和对依赖模块的执行 时机的处理不同。

// CMD
define(function(require, exports, module) { 
  var a = require("./a"); 
  a.doSomething(); 
  // 此处略去 100 行 
  var b = require("./b"); // 依赖可以就近书写 
  b.doSomething(); // ...
});
1
2
3
4
5
6
7
8

# 26-4、ES6

使用 import 和 export 的形式来导入导出模块。

# 27. documen.write 和 innerHTML 的区别

document.write 的内容会代替整个文档内容,会重写整个页面。

innerHTML 的内容只是替代指定元素的内容,只会重写页面中的部分内容

# 28. V8下的垃圾回收机制

V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代老生代两部分。

  • 新生代空间: 用于存活较短的对象,又分成两个空间: from 空间 与 to 空间
  • Scavenge GC算法: 当 from 空间被占满时,启动 GC 算法
    1、存活的对象从 from space 转移到 to space
    2、清空 from space
    3、from space 与 to space 互换
    4、完成一次新生代GC

在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。
新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。
当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。

  • 老生代空间: 用于存活时间较长的对象

  • 从 新生代空间 转移到 老生代空间 的条件,满足其一即可
    1、经历过一次以上 Scavenge GC 的对象
    2、当 to space 体积超过25%

  • 标记清除算法: 标记存活的对象,未被标记的则被释放
    增量标记: 小模块标记,在代码执行间隙,GC 会影响性能
    并发标记(最新技术): 不阻塞 js 执行

  • 标记压缩算法:: 将内存中清除后导致的碎片化对象往内存堆的一端移动,解决 内存的碎片化

# 29. 0.1 + 0.2 != 0.3

console.log(0.1 + 0.2 === 0.3); // false
console.log(0.1 + 0.2); // 0.30000000000000004
1
2

JS数字采用IEEE 754 双精度 64 位浮点数来存储,在js中数字只能保存小数点后52位,第53位为0省略,

  • 原生办法解决:
console.log(parseFloat((0.1 + 0.2).toFixed(10)) === 0.3); // true
console.log(parseFloat((0.1 + 0.2).toFixed(10))); // 0.3
1
2

# 30. 性能定义和性能优化

参考 (opens new window)

  • 性能优化分为两个大的分类 1、加载时优化
    2、运行时优化

# 30-1、加载时性能

顾名思义加载时优化 主要解决的就是让一个网站加载过程更快,比如压缩文件大小、使用CDN加速等方式可以优化加载性能。

检查加载性能的指标一般看:白屏时间和首屏时间

白屏时间:指的是从输入网址, 到页面开始显示内容的时间。
首屏时间:指从输入网址, 到首屏页面内容渲染完毕的时间。

  • 白屏时间计算

将代码脚本放在head标签前面就能获取白屏时间

<script>
    new Date().getTime() - performance.timing.navigationStart
</script>
1
2
3
  • 首屏时间计算

在window.onload事件中执行以下代码,可以获取首屏时间

new Date().getTime() - performance.timing.navigationStart
1

# 加载时性能优化

浏览器如果输入的是一个网址,首先要交给DNS域名解析 -> 找到对应的IP地址 -> 然后进行TCP连接 -> 浏览器发送HTTP请求 -> 服务器接收请求 -> 服务器处理请求并返回HTTP报文 -> 以及浏览器接收并解析渲染页面。
从这一过程中,其实就可以挖出优化点,缩短请求的时间,从而去加快网站的访问速度,提升性能。

这个过程中可以提升性能的优化的点:

1、DNS解析优化,浏览器访问DNS的时间就可以缩短
2、使用HTTP2
3、减少HTTP请求数量
4、减少http请求大小
5、服务器端渲染
6、静态资源使用CDN
7、资源缓存,不重复加载相同的资源

  • 1、DNS 预解析

用meta信息来告知浏览器, 当前页面要做DNS预解析

<meta http-equiv="x-dns-prefetch-control" content="on" />
1

在页面header中使用link标签来强制对DNS预解析

<link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />
1

注意:dns-prefetch需慎用,多页面重复DNS预解析会增加重复DNS查询次数。

  • 2、使用HTTP2

HTTP2带来了非常大的加载优化,所以在做优化上首先就想到了用HTTP2代替HTTP1。

HTTP2相对于HTTP1有这些优点:解析速度快、多路复用、首部压缩、服务器推送

  • 3、减少HTTP请求数量

HTTP请求建立和释放需要时间。

HTTP请求从建立到关闭一共经过以下步骤:

1、客户端连接到Web服务器
2、发送HTTP请求
3、服务器接受请求并返回HTTP响应
4、释放连接TCP链接

  • 4、压缩、合并文件

压缩文件 -> 减少HTTP请求大小,可以减少请求时间
文件合并 -> 减少HTTP请求数量。

我们可以对html、css、js以及图片资源进行压缩处理,现在可以很方便的使用 webpack 实现文件的压缩

JS压缩:UglifyPlugin
CSS压缩:MiniCssExtractPlugin
HTML压缩:HtmlWebpackPlugin
图片压缩:image-webpack-loader

  • 5、服务端启用gzip,减少传输文件的体积

# 30-2、运行时性能

运行时性能是指页面运行时的性能表现,而不是页面加载时的性能。
可以通过chrome开发者工具中的 Performance 面板来分析页面的运行时性能。

# 31. 尾调用及其好处

尾调用指的是函数的最后一步调用另一个函数。我们代码执行是基于执行栈的, 所以当我们在一个函数里调用另一个函数时,我们会保留当前的执行上下文,然 后再新建另外一个执行上下文加入栈中。
使用尾调用的话,因为已经是函数的最 后一步,所以这个时候我们可以不必再保留当前的执行上下文,从而节省了内存, 这就是尾调用优化。
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

# 32.cookies,sessionStorage 和 localStorage 的区别

共同点:都是保存在浏览器端,且同源的

特性​ ​​Cookies​ ​​sessionStorage​ ​​localStorage​
​​存储容量​ 4KB 左右(每个域名) 5-10MB(不同浏览器不同) 5-10MB(甚至更大)
生命周期​ 可设置过期时间(默认会话级,关闭浏览器后清除)
通过 expires 或 max-age 设置过期时间
​​会话级​​(关闭标签页清除) ​永久存储​​(需手动清除)
作用域​ 同域名下所有标签页共享 ​仅当前标签页​ 同域名下所有标签页共享
同步行为 修改后立即同步 完全不共享 需通过 storage 事件监听同步
​​是否随请求发送​ 是(自动附加到 HTTP 请求头)
​典型用途 用户认证、会话跟踪 临时表单数据、页面间传参 长期存储(如用户偏好设置)
安全注意事项 敏感数据应标记 HttpOnly 和 Secure(防止 XSS 和嗅探) 不要存储敏感信息(如密码),易受 XSS 攻击 不要存储敏感信息(如密码),易受 XSS 攻击

# Cookies​​:

// 设置 Cookie(需手动处理字符串) 
// Cookies​还有路径(path)的概念,可以限制 cookie 只属于某个路径下
document.cookie = "username=John; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/";
1
2
3

# sessionStorage/localStorage​​:

// 使用 Web Storage API(简单易用)
localStorage.setItem('theme', 'dark');
sessionStorage.setItem('tempData', JSON.stringify({ id: 1 }));
1
2
3

# 33.​​IndexedDB​​ 以及 Service Worker + Cache API​

​​维度​ ​IndexedDB​ Service Worker + Cache API​
​​数据用途​ 动态结构化数据(如用户数据、日志) 静态资源或网络请求响应(如 HTML、API)
​​离线支持​ 需配合 Service Worker 实现完整离线功能 直接支持离线资源加载
查询能力​ 支持复杂查询(索引、游标) 仅能通过 URL 匹配缓存
​​适用场景​ 数据库类应用(如本地笔记、编辑器) 离线优先的 Web 应用(如新闻、文档阅读)

# IndexedDB:客户端大规模结构化数据存储​

  • 核心特性​​
    • ​类型​​:基于 JavaScript 的 NoSQL 数据库,支持索引、事务和异步操作。
    • ​容量​​:浏览器通常允许存储数百 MB 甚至 GB 级数据(取决于用户设备)。
    • 数据结构​​:存储键值对(值可以是复杂对象、文件二进制数据等)。
    • 异步 API​​:所有操作非阻塞,通过事件或 Promise 返回结果。 ​​
  • 适用场景​​
    • 需要存储大量结构化数据(如用户生成的文档、离线应用数据)。
    • 需要高性能查询(通过索引快速检索)。
    • 需要事务支持(保证数据一致性,如金融类应用)。
// 打开或创建数据库
const request = indexedDB.open('MyDatabase', 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  // 创建对象存储空间(类似表)
  const store = db.createObjectStore('users', { keyPath: 'id' });
  // 创建索引
  store.createIndex('nameIndex', 'name', { unique: false });
};

request.onsuccess = (event) => {
  const db = event.target.result;
  // 插入数据
  const transaction = db.transaction('users', 'readwrite');
  const store = transaction.objectStore('users');
  store.add({ id: 1, name: 'Alice', age: 30 });

  // 查询数据
  const getRequest = store.get(1);
  getRequest.onsuccess = () => {
    console.log(getRequest.result); // { id: 1, name: 'Alice', age: 30 }
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# Service Worker + Cache API:离线应用缓存​

  • ​Service Worker​​:

    • 独立于主线程的脚本,可拦截网络请求、管理缓存。
    • 支持离线运行和后台同步。
  • ​Cache API​​:

    • 提供缓存存储机制(键值对,键为请求对象,值为响应对象)。
    • 可缓存 HTML、CSS、JS、图片等静态资源或 API 响应
  • 适用场景​​

    • 构建离线可用的 PWA(渐进式 Web 应用)。
    • 加速重复访问的静态资源加载。
    • 实现自定义缓存策略(如“网络优先,失败后回退缓存”)。
lastUpdate: 5/5/2025, 8:38:10 AM