目录
1、内存泄漏
内存泄漏就是内存中的变量没有回收,一直存在与内存中,造成内存的浪费的行为。
1)意外的全局变量; 2)计时器和回调函数timers; 3)DOM泄漏;4)js闭包; 5)console
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<p id="p1">p1</p>
<!-- 内存泄漏:内存中的变量没有回收,一直存在与内存中,造成内存的浪费 -->
<script>
// 1. 意外的全局变量 这里的a就是全局变量 它是foo()内部作用域变量的引用
function foo() {
// 一个未声明变量的使用,会在全局对象中创建一个新的变量
a = "test";
console.log(a);
}
// 上面的写法等价于
// function foo() {
// window.a = "test";
// }
foo();
// 1.解决方法;清空这个变量 不过有缓存问题
a = null;
console.log(a);
</script>
<script>
// 2.解决方法:在js文件开头添加 ‘use strict',开启严格模式 不存在意外的全局变量
'use strict'
function foo() {
// 必须定义
let a = "test";
console.log(a);
}
foo()
// 这里的a变为局部变量
// console.log(a);
// 3.定时器setInterval或者setTimeout使用没有及时清理
// 定义一个叫timer的定时器
const timer=setInterval(()=>{
console.log(1);
},200)
// 清除叫timer的定时器
clearInterval(timer)
// 3. DOM泄漏
// 1)给DOM对象添加的属性是一个对象的引用
const obj={
a:1
}
// 给p1添加一个c属性 浅拷贝了对象obj给c obj变了c也跟着变化
document.getElementById('p1').c=obj
obj.d=5
console.log(document.getElementById('p1').c);
// 解决方法:在window.onload时间中加上 document.getElementById('id').c = null;
window.onload=()=>{
document.getElementById('p1').c = null;
console.log(document.getElementById('p1').c);
}
// 2)元素引用没有清理 直接删除元素
// 3)事件的绑定没有移除
const fn=()=>{
console.log(1);
}
document.getElementById('p1').addEventListener('click',fn)
removeEventListener('click',fn)
// 控制台打印的没有清除
// js闭包
// 闭包函数里面的局部变量没有销毁
</script>
</body>
</html>
2、箭头函数
箭头函数是匿名函数,不能作为构造函数;
不会创建自己的this,只会从自己的作用域链的上一层继承this;不能通过apply和call改变this
不能使用arguments,用rest参数解决;
没有原型prototype,,没有super用于访问原型属性
不可以使用yield,不能用作Generator函数
3、声明和定义
变量声明不开辟内存,只是告诉编译器要声明的部分存在,要预留部分的空间;var i
变量定义开辟内存;
4、垃圾回收机制
浏览器的js具有自动垃圾回收机制,垃圾回收机制也就是自动内存管理机制
垃圾:(1)没有被引用的对象或变量;(2)无法访问到的对象(几个对象引用形成一个环,互相引用)
可达性:指那些以某种方式可以访问到或可以用到的值,它们被保证存储在内存中
垃圾回收机制(GC Garbage Collection):执行环境负责管理代码执行过程中使用的内存。JS的垃圾回收机制是为了以防内存泄漏(当已经不需要某块内存时这块内存还存在着,没有被释放,导致该内存无法被使用),垃圾回收机制就是间歇的不定期的寻找不再使用的变量,并释放掉它们所指向的内存;
JS中检测哪块内存可以被回收的方法:标记清除、引用计数
1)标记清除 最常用的垃圾回收方式
①当变量进入执行环境时(函数中声明变量),就标记这个变量为“进入环境”,当变量离开环境时(函数执行结束),则将其标记为“离开环境”
②垃圾回收器再运行的时候会给存储在内存中的所有变量都加上标记;
③去掉环境中的变量以及被环境中的变量引用的变量(闭包)的标记
④之后还存在标记的变量即是需要回收的变量
⑤最后,垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间
2)引用计数
跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1;如果该变量的值变成另外一个,则这个值的引用次数减1;当这个值的引用次数变为0时,说明这个值没有办法被访问,被视为准备回收的对象;每当过一段时间开始垃圾回收时,就把引用次数为0的值所占用的空间清理;
引用计数方法可能导致循环引用,类似死锁,导致内存泄漏。
4、不建议在JS中使用innerHTML
通过innerHTML修改内容,每次都会刷新,因此很慢;在innerHTML中没有验证的机会,因此更容易在文档中插入错误代码,使网页不稳定。
5、null和undefined的区别
null是一个表示“无”的对象,转为数值时为0;
undefined是一个表示“无”的原始值,转为数值时为NaN
6、new操作符的作用
1)创建一个空对象
2)将新对象与构造函数通过原型链连接(obj.__proto__ = fn.prototype)
3)将构造函数的this绑定到新对象上(fn.apply(obj,args))
4)根据构造函数的返回类型做判断,如果是值,返回对象obj;如果是引用类型,返回这个引用类型的对象
function newFunc(fn,...args) {
// 1.创建一个新对象
let obj = {}
// 2.将新对象和构造函数通过原型链连接
obj.__proto__ = fn.prototype
// 3.将构造函数的this绑定到新对象上
const result = fn.apply(obj,args)
// 4.根据返回值类型判断,如果是值类型返回obj,如果是引用类型返回正常引用类型
return result instanceof Object ? result : obj
7、函数作用域和块作用域
函数作用域:属于这个函数的任何声明(变量和函数)都可以在这个函数的范围内使用及复用(包括这个函数嵌套的作用域)
块级作用域:当代码块中存在let或const,这个代码块就成为块级作用域(定义变量和使用变量只能在同一个{}内);
8、JS中的继承方式
继承可以使得子类具有父类的各种方法和属性;
1)原型链继承
构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针
-
-
- 定义父类型构造函数
- 给父类型的原型添加方法
- 定义子类型的构造函数
- 创建父类型的实例对象赋值给子类型的原型
- 将子类型原型的构造属性设置为子类型
- 创建子类型的对象:可以调用父类型的方法
-
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
this.subproperty = false;
}
// 这里是关键,创建SuperType的实例,并将该实例赋值给SubType.prototype
SubType.prototype = new SuperType();
// 让子类型的原型的constructor指向子类型
SubType.prototype.constructor = SubType
SubType.prototype.getSubValue = function() {
return this.subproperty;
}
var instance = new SubType();
console.log(instance.getSuperValue()); // true
缺点:多个实例对引用类型的对象会被篡改
2)借用构造函数继承
使用父类的构造函数来增强子类的实例,等同复制父类的实例给子类
-
-
- 定义父类型构造函数
- 定义子类型构造函数
- 在子类型构造函数中调用父类型构造
-
function SuperType(){
this.color=["red","green","blue"];
}
function SubType(){
//继承自SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color);//"red,green,blue,black"
var instance2 = new SubType();//SubType创建子类实例时调用SuperType构造函数,SubType的每个实例都会将SuperType中的属性复制一份
alert(instance2.color);//"red,green,blue"
缺点:
只能继承父类的实例属性和方法,不能继承原型属性和方法
无法实现复用,每个子类都有父类实例函数的副本,影响性能
3)组合继承
用原型链实现对原型属性和方法的继承,用借用构造函数实现实例属性的继承;
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
// 继承属性
// 第二次调用SuperType()
SuperType.call(this, name);
this.age = age;
}
// 继承方法
// 构建原型链
// 第一次调用SuperType()
SubType.prototype = new SuperType();
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
缺点:
- 第一次调用
SuperType()
:给SubType.prototype
写入两个属性name,color。 - 第二次调用
SuperType()
:给instance1
写入两个属性name,color。
实例对象instance1
上的两个属性就屏蔽了其原型对象SubType.prototype的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。
4)原型式继承:利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4);
person5.friends.push("lucy");
console.log(person4.name); //tom
console.log(person4.name === person4.getName());//true
console.log(person5.name);//parent4
console.log(person4.friends);//["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends);["p1", "p2", "p3","jerry","lucy"]
Object.create()接收两个参数,第一个参数是作为新对象原型的队形,第二个可选参数是为新对象定义额外属性的对象,该方法只能实现浅拷贝,所以最后两个输出结果一样
5)寄生式继承
使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承。
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends
};
return clone;
}
let person5 = clone(parent5);
console.log(person5.getName());
console.log(person5.getFriends());
6)寄生组合式继承
function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent6() {
this.name = 'parent6';
this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
return this.name;
}
function Child6() {
Parent6.call(this);
this.friends = 'child5';
}
clone(Parent6, Child6);
Child6.prototype.getFriends = function () {
return this.friends;
}
let person6 = new Child6();
console.log(person6);
console.log(person6.getName());
console.log(person6.getFriends());
7)ES6中extends关键字实现
class Person {
constructor(name) {
this.name = name
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
console.log('Person:', this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(name)
this.age = age
}
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法
9、深拷贝和浅拷贝
数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和对象数据类型。
- 基本数据类型的特点:直接存储在栈(stack)中的数据
- 引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
浅拷贝和赋值的区别:
- 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
- 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
赋值
// 对象赋值
var obj1 = {
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)
浅拷贝
// 浅拷贝
var obj1 = {
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj3 = shallowCopy(obj1);
obj3.name = "lisi";
obj3.language[1] = ["二","三"];
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
console.log('obj1',obj1)
console.log('obj3',obj3)
实现浅拷贝的方式:1)Object.assign({},obj) 当obj中的属性都为基本数据类型时为深拷贝
2) arr.concat() 3)arr.slice()
实现深拷贝的方式:1)JSON.parse(JSON.stringify(obj))
用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。但不能处理函数
let arr = [1, 3, {
username: ' kobe'
},function(){}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)
2)ES6展开语法 {...obj}
3)手写递归
function clone(origin){
let obj={};
if(origin instanceof Array){
obj=[];
}
for(let key in origin){
let value=origin[key];
obj[key]=(typeof value==="object" && value !== null)?clone(value):value
}
return obj
}