闭包
理解
简而言之,就是在函数内部再创建函数,并通过内部函数调用外部函数的数据,其带来了两个最重要的优势:
1.使得函数能够调用外部函数的变量,从而避免了变量污染导致的问题
2.支持函数以值的形式传递,是函数式编程的基础
例如网上常用的计数器函数示例:
let i = 0;
function count(){
return i++;
}
console.log(count())
console.log(count())
console.log(count())
但这样很容易造成变量污染(特别是在es6之前还都是用var
来定义变量,使得别的地方如果有重名变量会很容易对其进行修改),但如果将变量移动到函数内部,如下面代码:
function count(){
let i = 0;
return i++;
}
那么可以明显看出,每次调用函数的时候都会重新定义一次变量,导致结果永远为0。由此引出闭包的概念:在函数内部创建函数来访问上层函数的变量,即修改如下:
function count(){
let i = 0;
return function(){
return i++;
}
}
counter = count()
console.log(counter) // [Function]
console.log(counter()) // 0
console.log(counter()) // 1
console.log(counter()) // 2
此时可以看出count()
函数我们只调用了一次,并返回一个能够操作变量i
的函数,接下来只需要不停的调用这个函数即可实现计数功能
问题
虽然闭包解决了一些特定场景的问题,但也要慎用,例如上面的函数里我们可以看到之后counter
可以不断地调用实现计数器功能,但之所以能够一直调用就是因为没有被垃圾回收机制给回收,这也引出了一些隐患,如:内存泄漏,所以在不使用该函数时记得将其销毁
基于闭包实现封装+抽象
如果某个属性或者方法不希望被外部访问(抽象处理),可以利用闭包特性,定义一个变量保存这些内容,举例:
function Test() {
let data = {name: "aaa"};
this.getName = function() { return data.name; }
}
test = new Test();
console.log(test.getName());
原型
在ES6推出class
语法糖以后,再自己使用到原型的机会已经很少了,但该语法糖的本质也是利用了原型的知识点实现的,所以还是有必要了解一下的
概念参考
创建没有原型的对象
可以通过Object.create
方法创建对象,第一个参数指定对象原型,第二个参数为对象本身,我们可以指定原型为null
来实现,举例:
let o1 = Object.create(null, {
x: {
value: 1
}
});
let o2 = {};
console.log(o1.__proto__, o2);
// undefined {}
可以看到默认的原型指向Object
,如果创建时,指定原型为空,则创建出来的对象没有原型
prototype
/__proto__
区别
prototype
:类里的属性(类的本质还是通过函数写的,所以也可以理解成函数里的属性)
__proto__
:实例化对象里的属性(直接使用较不规范,一般用Object.getPrototypeOf
代替)
设置原型(实现继承关系)
通过Object.setPrototypeOf
方法实现,举例:
let a = {};
let b = {};
console.log(a.__proto__ === Object.prototype) // true
Object.setPrototypeOf(a, b);
// 将a的原型指向b
console.log(a.__proto__ === Object.prototype) // false
console.log(a.__proto__ === b) // true
查看原型
通过Object.getPrototypeOf
查看,举例:
Object.getPrototypeOf({});
// {}
原型关系判断
-
a instanceOf B
:判断B类是否在a的原型链上 -
a.isPrototypeOf(b)
:判断a对象是否在b对象的原型链上,举例:
let arr = new Array();
Object.prototype.isPrototypeOf(arr); // true
// Object的类对象(构造方法)在所有对象的原型链上
-
x in a
:x是否为a的属性,或者在a的原型链上 -
a.hasOwnPrototype(x)
:x是否为a的属性(不判断a的原型链)
创建对象方式演变
-
Object.create
:只能定义,不能获取原型 -
__proto__
:可以获取原型,但是是一种非标准的解决办法,本质上并不是一个属性,而是属性访问器,因此给其赋值时,会判断是否为对象,不是则无法成功赋值,举例:
let a = {};
console.log(a.__proto__); // {}
a.__proto__ = "aaa";
console.log(a.__proto__); // {}
a.__proto__ = {x:1};
console.log(a.__proto__); // { x: 1 }
-
Object.setPrototypeOf
:可以用getPrototypeOf
获取原型,然后在通过该方法设置原型,是较为规范的一种方式
多继承实现
定义多个对象,在里面封装需要继承的方法,通过Object.assign
合并当前对象和多个对象实现其他对象的功能继承,举例:
function a() {}
let b = {
func1() {
console.log(1);
}
};
let c = {
func2() {
console.log(2);
}
};
a.prototype = Object.assign(a.prototype, b, c);
new a().func1();
new a().func2();
注:
该方法中对于重名的属性,后面的会覆盖前面的
this指向问题
参考
彻底理解js中this的指向,不必硬背。
js中this的指向问题归纳总结
JavaScript中call,apply,bind方法的总结
箭头函数中的this指向
只跟当前定义的上下文有关,跟谁调用他无关,并且无法通过
call
/apply
/bind
方法指定this
下面是个人理解:其实箭头函数里并没有this
,因此其会从父作用域中寻找this
作为其自身的this
,看下面示例应该就能懂了:
let window_this = this;
o = {
o_this: this,
x: function(){
let x_this = this;
(() => {
console.log(x_this === this, window_this === this);
})();
},
y: () => {
let y_this = this;
(() => {
console.log(y_this === this, window_this === this);
})()
}
}
console.log(o.o_this === window_this);
o.x();
o.x.call({});
o.y();
// true
// true false
// true false
// true true
- 针对第一个结果可以看出对象
o
的this
指向window
对象(具体原理可以参考前面推荐的this指向相关文章,这里主要针对箭头函数的this指向进行讨论) - 针对后三个结果:
可以看出第一个判断结果都为true
,即:箭头函数中的this
和父级作用域中的this
一直是相等的,当使用call
/apply
/bind
改变父级this
的指向的,箭头函数的指向也会跟着一起改变(应该说永远跟父级作用域中的this
指向相同)
可以看出第二个判断结果前两个为false
,最后一个为true
:因为x
函数的this
指向对象o
,而x
函数中箭头函数的this
也跟x
函数一样,因此都不为window
对象;y
函数因为是箭头函数,其this
则为对象o
的this
,而o
的this
是window
对象,同理y
里面的箭头函数跟y
函数一样,因此都为window
对象
参考:
https://blog.csdn.net/weixin_41845146/article/details/84325110
https://www.jb51.net/article/163384.htm
node环境/浏览器环境this指向
https://blog.csdn.net/qq_33594380/article/details/82254834
堆栈内存
栈内存
- 存储基本数据类型的数据和引用类型的指针
- 给代码提供运行环境
- 每当形成一个作用域,就会消耗一块栈内存
- 函数每执行一次,就会创建一个新的栈内存
- 栈内存回收,那么栈内存下所有存储的值都会一起被回收
堆内存
- 存储引用数据类型
- 会首先把属性名是数字的按从小到大的顺序放入堆内存中
垃圾回收机制
不同浏览器机制
chrome
标记法:每隔一段时间就对所有指针进行一次检测,如果该指针没有被占用,则回收
IE
引用计数法:数据地址每被引用一次,就会计数+1,反之-1,如果计数为0,则立即将该地址内容回收
不同作用域下回收机制
作用域就是栈内存,分为:全局、私有
全局作用域
当打开页面时,会创建一个全局作用域,该作用域只有关闭页面或浏览器时才会被销毁
私有作用域
在全局作用域下形成的,所有全局作用域下包含所有私有作用域,私有作用域下的数据可能会被立即销毁,也可能不销毁,或者延迟销毁
立即销毁
举例:
function fun() {
let x = 1;
}
不销毁
几种不销毁的情况:
- 闭包
- 值被DOM绑定
- 返回一个引用类型数据
- 返回的值需要被外界接收
闭包举例:
function num() {
let i = 0;
// i存在与num执行的栈内存里
return function() {
return i++;
}
}
n = num()
console.log(n())
console.log(n())
console.log(n())
值被DOM绑定:
function bind() {
let x = 1;
// 因为x的地址被DOM事件占用,因此不销毁
div.onclick = function() {
console.log(x);
}
}
返回一个引用类型数据:
let o = {
a:1,
func:(function() {
// 这个自执行函数返回的函数被func属性占用
let x = 1;
return function() {
console.log(x);
}
})
}
不立即销毁
举例:
function func() {
let x = 1;
return function() {
console.log(x);
}
}
func()()
此时要等到func里的小函数执行完毕以后,func的栈内存才会被销毁
参考:https://blog.csdn.net/qq_37200686/article/details/86487055