一、JS执行上下文和执行栈
执行上下文:浏览器的JavaScript引擎会创造一个特殊的环境来处理这些JavaScript代码的转换和执行,js中运行任何代码都是在执行上下文中运行
执行上下文类型:全局、函数、eval
执行上下文的生命周期:创建阶段(this绑定、词法环境、变量环境)、执行阶段(编译、代码执行)、回收阶段
执行上下文栈:先进后出原则(进栈出栈)
二、JS内存
栈数据结构-基本类型:后进先出
堆数据结构-引用类型:树状结构
队列:先进先出(事件循环 Event Loop)
垃圾回收:引用计数(不再使用:害怕循环引用,相互引用造成内存泄漏)、标记清除(常用,无法到达的对象,从根部扫描,从根部无法到达的对象标记不再使用)
注意:闭包中的变量保存在堆内存中, 返回函数后闭包还能引用到函数內的变量
function A() {
let a = 1
function B() {//闭包 函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包
console.log(a)
}
return B
}
内存泄漏:不再用到的内存,没有及时释放,就叫做内存泄漏
常见的js内存泄漏:
(1)意外的全局变量:例如未定义的变量会在全局创建新变量(或this指向导致)
function foo(arg) {
bar = "this is a hidden global variable"; // 挂载到全局对象上,意外创建全局变量
}
function foo() {
this.variable = "potential accidental global"; //this 指向了全局对象(window)
}// 而不是 undefined
foo();
// 办法:在 JavaScript 文件头部加上 'use strict',使用严格模式避免意外的全局变量
(2)被遗忘的计时器或回调函数
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
// 不管node是否移除,定时器一直存活且垃圾回收机制无法回收
(3)脱离 DOM的引用:不是在dom树中引用,例如数组或对象中引用
var elements = { // 要清除时两个引用都得 清除
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
// 另外注意:如果代码包含子dom的引用,清除整体时不会清楚子的引用,因为父子之前存在引用关系
(4)闭包:闭包的关键是匿名函数可以访问父级作用域的变量,可以手动设置null解决
三、作用域链和闭包
闭包:有权访问另一函数作用域中变量的函数
特点:可以访问当前函数以外的变量,即使外部函数已返回闭包仍能访问外部函数定义的变量,也可更新外部值
function getOuter(){
var date = '815';
function getDate(str){
date = str; // 可以修改变量的值
console.log(str + date); //访问外部的date
}
return getDate; //外部函数返回,正常
}
var today = getOuter();
today('今天是:'); //"今天是:815"
today('明天不是:'); //"明天不是:815"
作用域链:一层层拿变量(向上拿变量)
// var引入的问题
var scope="global";
function scopeTest(){
console.log(scope); // 变量提升,拿到undefined,但不会报错
var scope="local"
}
scopeTest(); //undefined
// 没有块级作用域
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 3
data[1](); // 3
data[2](); // 3
// 办法使用:返回一个 匿名函数赋值
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (num) {
return function(){
console.log(num);
}
})(i);
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
// 办法使用let块级作用域
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
四、this解析
优先级:
独立调用优先级最低(默认调用,函数调用,非严格window,严格undefined)
显示调用(call apply bind)优先级高于隐式调用(对象中的函数)
new绑定优先级高于隐式调用
new绑定优先级高于显示调用
五、深浅拷贝原理
1、赋值:
(1)基本数据类型:赋值,赋值之后互不影响
(2)引用数据类型:赋址,变量有相同的引用,互相间有影响
let a = "muyiy";
let b = a;
console.log(b); // muyiy
a = "change";
console.log(a); // change
console.log(b); // muyiy
// 引用类型相互受影响
let a = {
name: "muyiy",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let b = a;
console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "45"}
// }
a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "You Don't Know JS", price: "55"}
// }
console.log(b);
// {
// name: "change",
// book: {title: "You Don't Know JS", price: "55"}
// }
2、浅拷贝:基本类型拷贝就是值(互相不影响),引用类型拷贝的是地址,相互之间有影响
浅拷贝场景:
(1)简单的赋值实现
(2)Object.assign()实现,任意多个的源对象自身的可枚举属性拷贝给目标对象(一层是深拷贝)
(3)函数库lodash的_.clone方法
(4)扩展运算符 …
(5)Array.prototype.concat()
(6)Array.prototype.slice()
let a = {
name: "muyiy",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let b = Object.assign({}, a);
console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "45"}
// }
a.name = "change";
a.book.price = "55";
console.log(a);
// {
// name: "change",
// book: {title: "You Don't Know JS", price: "55"}
// }
console.log(b);
// {
// name: "muyiy",
// book: {title: "You Don't Know JS", price: "55"}
// }
3、深拷贝:拷贝所有的属性并拷贝 属性所指向的内存(前后所有的操作互不影响)
深拷贝使用场景:
(1)JSON.parse(JSON.stringify(object)) 对数组对象都可以
问题:会忽略symbol|undefined,不能序列化函数(忽略即执行 Object.create(oldObj))、不能正确处理new Date() 不能处理正则,不能解决循环引用的对象(报错)
所以遇到symbol、undefined、函数直接忽略
(2)若对象只有一层的话Object.assign()函数也可 实现
(3)lodash的.cloneDeep方法
(4) Object.create(oldObj)
(5)手动递归实现
var obj1 = { body: { a: 10 } };
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.body.a = 20;
console.log(obj1);
// { body: { a: 10 } } <-- 沒被改到
console.log(obj2);
// { body: { a: 20 } }
console.log(obj1 === obj2);
// false
console.log(obj1.body === obj2.body);
// false
问题:忽略函数、symbol、undefined,你能处理new Date()处理错误 不能解决循环引用的对象(报错)
// 木易杨
let obj = {
name: 'muyiy',
a: undefined,
b: Symbol('muyiy'),
c: function() {}
}
console.log(obj);
// {
// name: "muyiy",
// a: undefined,
// b: Symbol(muyiy),
// c: ƒ ()
// }
let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy"}
let obj = {
a: 1,
b: {
c: 2,
d: 3
}
}
obj.a = obj.b;
obj.b.c = obj.a;
let b = JSON.parse(JSON.stringify(obj));
// Uncaught TypeError: Converting circular structure to JSON
JSON.stringify(new Date());
// ""2018-12-24T02:59:25.776Z""
// 递归实现
function deepCopy(target) {
if (
Object.prototype.toString.call(target) !== '[object Object]' &&
Object.prototype.toString.call(target) !== '[object Array]'
) {
return target
}
const newTarget = Array.isArray(target) ? [] : {} // 给引用初始化结构
// 自身的可枚举的(不含symbol)若使用for in 则的过滤出自身的(不能包含继承的)
Object.keys(target).forEach(
(key) =>
(newTarget[key] =
target[key] instanceof Object ? deepCopy(target[key]) : target[key])
)
return newTarget
}
// 调用
deepCopy(obj)
六、原型Prototype
1、什么是构造函数
构造函数本身就是函数,与普通函数不同是首字母大写,使用 new 生成实例的函数就是构造函数,直接调用的就是普通函数
// 普通函数
function parent2(age) {
this.age = age;
}
var p2 = parent2(50); // 因为普通函数不用返回新创见的对象,需手动执行
// undefined
// 普通函数
function parent3(age) {
return {
age: age
}
}
var p3 = parent3(50);
p3.constructor === Object; // true
2、Symbol是构造函数吗?
Symbol作为构造函数不完整,因不能通过new创建,但原型上有constructor属性指向Symbol函数
var sym = Symbol(123);
console.log( sym );
// Symbol(123)
console.log( sym.constructor );
// ƒ Symbol() { [native code] }
3、constructor值只读吗?
(1)对于引用类型constructor是可修改的
(2)对于基本类型是只读的(null 和 undefined 是没有 constructor 属性的)
七、高阶函数
(1)有一个或多个函数输入
(2)有一个函数输出
八、节流防抖:对事件的触发频次进行限制
(1)节流:一定时间内只执行一次
(2)防抖:无论触发多少次只执行最后一次,在触发期间再触发不执行