背景javascript
if(window.fetchData&&window.fetchDataSomeKeyLength=== window.fetchData.length) return
const data = fetch(`${url}`
window.fetchData = data
window.fetchDataSomeKeyLength=data.someKey.length
processData(data)//data.someKey的length作了处理
复制代码
通俗易懂的话来说,js的基本类型使用用来存储值得,它们分配大小是有限度 在定义基本类型变量的时候它们的内存都被分配完成,前端
string
、number
、boolean
和 symbol
这四种类型统称为原始类型(Primitive) ,表示不能再细分下去的基本类型;symbol
表示独一无二的值,经过 Symbol
函数调用生成,因为生成的 symbol
值为原始类型,因此 Symbol
函数不能使用 new
调用;null
和 undefined
一般被认为是特殊值,这两种类型的值惟一,就是其自己。java
对象es6
数组web
函数面试
和基本类型区分开来。对象在逻辑上是属性的无序集合或者有序集合,是存放各类值的容器。对象值存储的是引用地址,因此和基本类型值不可变的特性不一样,对象值是可变的。算法
实际上在引用字符串的属性或方法时,会经过调用 new String()
的方式转换成对象,该对象继承了字符串的方法来处理属性的引用,一旦引用结束,便会销毁这个临时对象,这就是包装对象的概念。编程
不只仅只是字符串有包装对象的概念,数字和布尔值也有相对应的 new Number()
和 new Boolean()
包装对象。null
和 undefined
没有包装对象,访问它们的属性会报类型错误。数组
字符串、数字和布尔值经过构造函数显式生成的包装对象,既然属于对象,和基本类型的值必然是有区别的,这点能够经过 typeof
检测出来。安全
typeof 'seymoe' // 'string'
typeof new String('seymoe') // 'object'
复制代码
typeof
instanceof
Object.prototype.toString()
typeof 'seymoe' // 'string'
typeof true // 'boolean'
typeof 10 // 'number'
typeof Symbol() // 'symbol'
typeof null // 'object'
没法断定是否为 null
typeof undefined // 'undefined'
复制代码
若是使用 typeof
操做符对对象类型及其子类型,譬如函数(可调用对象)、数组(有序索引对象)等进行断定,则除了函数都会获得 object
的结果。
typeof {} // 'object'
typeof [] // 'object'
typeof(() => {})// 'function'
复制代码
因为没法得知一个值究竟是数组仍是普通对象,显然经过 typeof
判断具体的对象子类型远远不够。
注意:instanceof
也不是万能的。其原理就是测试构造函数
var a={}
a.__proto__=[]
a instanceof Array //true
a instanceof Object //true
复制代码
Object.prototype.toString.call({}) // '[object Object]'
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call(() => {}) // '[object Function]'
Object.prototype.toString.call('seymoe') // '[object String]'
Object.prototype.toString.call(1) // '[object Number]'
Object.prototype.toString.call(true) // '[object Boolean]'
Object.prototype.toString.call(Symbol()) // '[object Symbol]'
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
Object.prototype.toString.call(new Date()) // '[object Date]'
Object.prototype.toString.call(Math) // '[object Math]'
Object.prototype.toString.call(new Set()) // '[object Set]'
Object.prototype.toString.call(new WeakSet()) // '[object WeakSet]'
Object.prototype.toString.call(new Map()) // '[object Map]'
Object.prototype.toString.call(new WeakMap()) // '[object WeakMap]'
复制代码
ToPrimitive 算法在执行时,会被传递一个参数 hint
,表示这是一个什么类型的运算(也能够叫运算的指望值),根据这个 hint
参数,ToPrimitive 算法来决定内部的执行逻辑。
hint
参数的取值只能是下列 3 者之一:
string
number
default
调用obj[Symbol.toPrimitive](hint)
- 带有符号键Symbol.toPrimitive
(系统符号)的方法,若是存在这样的方法,
不然若是提示是 "string"
obj.toString()
和obj.valueOf()
,不管存在什么。不然,若是提示是"number"
或"default"
obj.valueOf()
和obj.toString()
,不管存在什么。咱们提到了 ToPrimitive 算法中用到的 hint
参数,那怎样肯定一次运算场景下的 hint
取值是什么呢?很简单----新建一个对象,打印各个运算场景下的 hint
值:
let obj = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
console.log(`hint: ${hint}`);
}
};
alert(obj) // hint: string
+obj // hint: number
obj + 500 // hint: default
// 一个没有提供 Symbol.toPrimitive 属性的对象,参与运算时的输出结果
var obj1 = {};
console.log(+obj1); // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ""); // "[object Object]"
// 接下面声明一个对象,手动赋予了 Symbol.toPrimitive 属性,再来查看输出结果
var obj2 = {
[Symbol.toPrimitive](hint) {
if (hint == "number") {
return 10;
}
if (hint == "string") {
return "hello";
}
return true;
}
};
console.log(+obj2); // 10 -- hint 参数值是 "number"
console.log(`${obj2}`); // "hello" -- hint 参数值是 "string"
console.log(obj2 + ""); // "true" -- hint 参数值是 "default"
复制代码
但要注意下面两点:
Symbol.toPrimitive
和 toString
方法的返回值必须是基本类型值。valueOf
方法除了能够返回基本类型值,也能够返回其余类型值。当咱们建立一个普通对象时({}
或 new Object()
的方式等),对象上是不具有 [Symbol.toPrimitive]
(方法)属性的。因此,对于普通对象的到基本类型值的运算,通常按照具体场景:
hint
值为 "string"
时,先调用 toString
,toString
若是返回一个基本类型值了,则返回、终止运算;不然接着调用 valueOf
方法。valueOf
,valueOf
若是返回一个基本类型值了,则返回、终止运算;不然接着调用 toString
方法。栈是一种特殊的列表,栈内的元素只能经过列表的一端访问,这一端称为栈顶。 栈被称为是一种后入先出(LIFO,last-in-first-out)的数据结构。 因为栈具备后入先出的特色,因此任何不在栈顶的元素都没法访问。 为了获得栈底的元素,必须先拿掉上面的元素。
在这里,为方便理解,经过类比乒乓球盒子来分析栈的存取方式。
这种乒乓球的存放方式与栈中存取数据的方式一模一样。 处于盒子中最顶层的乒乓球 5,它必定是最后被放进去,但能够最早被使用。 而咱们想要使用底层的乒乓球 1,就必须将上面的 4 个乒乓球取出来,让乒乓球1处于盒子顶层。 这就是栈空间先进后出,后进先出的特色。
基本数据类型保存在栈内存中,由于基本数据类型占用空间小、大小固定,经过按值来访问,属于被频繁使用的数据。 为了更好的搞懂基本数据类型变量与栈内存,咱们结合如下例子与图解进行理解:
let num1 = 1;
let num2 = 1;
复制代码
引用数据类型存储在堆内存中,由于引用数据类型占据空间大、大小不固定。 若是存储在栈中,将会影响程序运行的性能; 引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。 当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中得到实体
// 基本数据类型-栈内存
let a1 = 0;
// 基本数据类型-栈内存
let a2 = 'this is string';
// 基本数据类型-栈内存
let a3 = null;
// 对象的指针存放在栈内存中,指针指向的对象存放在堆内存中
let b = { m: 20 };
// 数组的指针存放在栈内存中,指针指向的数组存放在堆内存中
let c = [1, 2, 3];
复制代码
所以当咱们要访问堆内存中的引用数据类型时,实际上咱们首先是从变量中获取了该对象的地址指针, 而后再从堆内存中取得咱们须要的数据。
let a = 20;
let b = a;
b = 30;
console.log(a); // 此时a的值是50
复制代码
在这个例子中,a、b 都是基本类型,它们的值是存储在栈内存中的,a、b 分别有各自独立的栈空间, 因此修改了 b 的值之后,a 的值并不会发生变化。
let m = { a: 10, b: 20 };
let n = m;
n.a = 15;
console.log(m.a) //此时m.a的值是多少,是10?仍是15?
复制代码
在这个例子中,m、n都是引用类型,栈内存中存放地址指向堆内存中的对象, 引用类型的复制会为新的变量自动分配一个新的值保存在变量中, 但只是引用类型的一个地址指针而已,实际指向的是同一个对象, 因此修改 n.a 的值后,相应的 m.a 也就发生了改变。
在JS中,基本数据类型变量大小固定,而且操做简单容易,因此把它们放入栈中存储。 引用类型变量大小不固定,因此把它们分配给堆中,让他们申请空间的时候本身肯定大小,这样把它们分开存储可以使得程序运行起来占用的内存最小。
栈内存因为它的特色,因此它的系统效率较高。 堆内存须要分配空间和地址,还要把地址存到栈中,因此效率低于栈。
在C语言和C++语言中,咱们若是想要开辟一块堆内存的话,须要先计算须要内存的大小,而后本身经过malloc函数去手动分配,在用完以后,还要时刻记得用free函数去清理释放,不然这块内存就会被永久占用,形成内存泄露。
可是咱们在写JavaScript的时候,却没有这个过程,由于人家已经替咱们封装好了,V8引擎会根据你当前定义对象的大小去自动申请分配内存。
不须要咱们去手动管理内存了,因此天然要有垃圾回收,不然的话只分配不回收,岂不是没多长时间内存就被占满了吗,致使应用崩溃。
垃圾回收的好处是不须要咱们去管理内存,把更多的精力放在实现复杂应用上,但坏处也来自于此,不用管理了,就有可能在写代码的时候不注意,形成循环引用等状况,致使内存泄露。
当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,由于只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
可使用任何方式来标记变量。好比,能够经过翻转某个特殊的位来记录一个变量什么时候进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪一个变量发生了变化。如何标记变量并不重要,关键在于采起什么策略。
目前,IE、Firefox、Opera、Chrome和Safari的JavaScript实现使用的都是标记清除式的垃圾回收策略(或相似的策略),只不过垃圾收集的时间间隔互有不一样。
若是同一个值又被赋给另外一个变量,则该值的引用次数加1。相反,若是包含对这个值引用的变量改变了引用对象,则该值引用次数减1。
当这个值的引用次数变成0时,则说明没有办法再访问这个值了,于是就能够将其占用的内存空间回收回来。
这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
循环引用是指对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用,看个例子:
复制代码
function foo () {
var objA = new Object();
var objB = new Object();
objA.otherObj = objB;
objB.anotherObj = objA;
}
复制代码
这个例子中,objA和objB经过各自的属性相互引用,也就是说,这两个对象的引用次数都是2。
在采用标记清除策略的实现中,因为函数执行后,这两个对象都离开了做用域,所以这种相互引用不是问题。
但在采用引用次数策略的实现中,当函数执行完毕后,objA和objB还将继续存在,由于它们的引用次数永远不会是0。加入这个函数被重复屡次调用,就会致使大量内存没法回收
还要注意的是,咱们大部分人时刻都在写着循环引用的代码,看下面这个例子,相信你们都这样写过:
var el = document.getElementById('#el');
el.onclick = function (event) {
console.log('element was clicked');
}
复制代码
咱们为一个元素的点击事件绑定了一个匿名函数,咱们经过event参数是能够拿到相应元素el的信息的。
你们想一想,这是否是就是一个循环引用呢? el有一个属性onclick引用了一个函数(其实也是个对象),函数里面的参数又引用了el,这样el的引用次数一直是2,即便当前这个页面关闭了,也没法进行垃圾回收。
若是这样的写法不少不少,就会形成内存泄露。咱们能够经过在页面卸载时清除事件引用,这样就能够被回收了
var el = document.getElementById('#el');
el.onclick = function (event) {
console.log('element was clicked');
}
// ...
// ...
// 页面卸载时将绑定的事件清空
window.onbeforeunload = function(){
el.onclick = null;
}
复制代码
因此,V8采用了一种代回收的策略,将内存分为两个生代:新生代(new generation)和老生代(old generation) 。
新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象,分别对新老生代采用不一样的垃圾回收算法来提升效率,对象最开始都会先被分配到新生代(若是新生代内存空间不够,直接分配到老生代),新生代中的对象会在知足某些条件后,被移动到老生代,这个过程也叫晋升,后面我会详细说明。
默认状况下,32位系统新生代内存大小为16MB,老生代内存大小为700MB,64位系统下,新生代内存大小为32MB,老生代内存大小为1.4GB。
新生代平均分红两块相等的内存空间,叫作semispace,每块内存大小8MB(32位)或16MB(64位)。
新生代存的都是生存周期短的对象,分配内存也很容易,只保存一个指向内存空间的指针,根据分配对象的大小递增指针就能够了,当存储空间快要满时,就进行一次垃圾回收。
新生代采用Scavenge垃圾回收算法,在算法实现时主要采用Cheney算法。 Cheney算法将内存一分为二,叫作semispace,一块处于使用状态,一块处于闲置状态。
处于使用状态的semispace称为From空间,处于闲置状态的semispace称为To空间。
接下来我会结合流程图来详细说明Cheney算法是怎么工做的。 垃圾回收在下面我统称为 GC(Garbage Collection) 。 step1. 在From空间中分配了3个对象A、B、C
step2. GC进来判断对象B没有其余引用,能够回收,对象A和C依然为活跃对象
step3. 将活跃对象A、C从From空间复制到To空间
step4. 清空From空间的所有内存
step5. 交换From空间和To空间
step6. 在From空间中又新增了2个对象D、E
step7. 下一轮GC进来发现对象D没有引用了,作标记
step8. 将活跃对象A、C、E从From空间复制到To空间
step9. 清空From空间所有内存
step10. 继续交换From空间和To空间,开始下一轮
经过上面的流程图,咱们能够很清楚的看到,进行From和To交换,就是为了让活跃对象始终保持在一块semispace中,另外一块semispace始终保持空闲的状态。
Scavenge因为只复制存活的对象,而且对于生命周期短的场景存活对象只占少部分,因此它在时间效率上有优异的体现。Scavenge的缺点是只能使用堆内存的一半,这是由划分空间和复制机制所决定的。
因为Scavenge是典型的牺牲空间换取时间的算法,因此没法大规模的应用到全部的垃圾回收中。但咱们能够看到,Scavenge很是适合应用在新生代中,由于新生代中对象的生命周期较短,偏偏适合这个算法。
对象重新生代移动到老生代的过程叫做晋升。
对象晋升的条件主要有两个:
因此,V8在老生代中主要采用了Mark-Sweep和Mark-Sweep相结合的方式进行垃圾回收。
与Scavenge不一样,Mark-Sweep并不会将内存分为两份,因此不存在浪费一半空间的行为。Mark-Sweep在标记阶段遍历堆内存中的全部对象,并标记活着的对象,在随后的清除阶段,只清除没有被标记的对象。
也就是说,Scavenge只复制活着的对象,而Mark-Sweep只清除死了的对象。活对象在新生代中只占较少部分,死对象在老生代中只占较少部分,这就是两种回收方式都能高效处理的缘由。
step1. 老生代中有对象A、B、C、D、E、F
step2. GC进入标记阶段,将A、C、E标记为存活对象
step3. GC进入清除阶段,回收掉死亡的B、D、F对象所占用的内存空间
能够看到,Mark-Sweep最大的问题就是,在进行一次清除回收之后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配形成问题。
若是出现须要分配一个大内存的状况,因为剩余的碎片空间不足以完成这次分配,就会提早触发垃圾回收,而此次回收是没必要要的。
**Mark-Compact是标记整理的意思,**是在Mark-Sweep的基础上演变而来的。Mark-Compact在标记完存活对象之后,会将活着的对象向内存空间的一端移动,移动完成后,直接清理掉边界外的全部内存。以下图所示: step1. 老生代中有对象A、B、C、D、E、F(和Mark—Sweep同样)
step2. GC进入标记阶段,将A、C、E标记为存活对象(和Mark—Sweep同样)
step3. GC进入整理阶段,将全部存活对象向内存空间的一侧移动,灰色部分为移动后空出来的空间
step4. GC进入清除阶段,将边界另外一侧的内存一次性所有回收
在V8的回收策略中,Mark-Sweep和Mark-Conpact二者是结合使用的。
因为Mark-Conpact须要移动对象,因此它的执行速度不可能很快,在取舍上,V8主要使用Mark-Sweep,在空间不足以对重新生代中晋升过来的对象进行分配时,才使用Mark-Compact。
V8的垃圾回收机制分为新生代和老生代。
新生代主要使用Scavenge进行管理,主要实现是Cheney算法,将内存平均分为两块,使用空间叫From,闲置空间叫To,新对象都先分配到From空间中,在空间快要占满时将存活对象复制到To空间中,而后清空From的内存空间,此时,调换From空间和To空间,继续进行内存分配,当知足那两个条件时对象会重新生代晋升到老生代。
老生代主要采用Mark-Sweep和Mark-Compact算法,一个是标记清除,一个是标记整理。二者不一样的地方是,Mark-Sweep在垃圾回收后会产生碎片内存,而Mark-Compact在清除前会进行一步整理,将存活对象向一侧移动,随后清空边界的另外一侧内存,这样空闲的内存都是连续的,可是带来的问题就是速度会慢一些。在V8中,老生代是Mark-Sweep和Mark-Compact二者共同进行管理的。
以上就是本文的所有内容,书写过程当中参考了不少中外文章,参考书籍包括朴大大的《深刻浅出NodeJS》以及《JavaScript高级程序设计》等。咱们这里并无对具体的算法实现进行探讨,感兴趣的朋友能够继续深刻研究一下。
最后,谢谢你们可以读到这里,若是文中有任何不明确或错误的地方,欢迎给我留言~~
标识符解析(变量查找),是按照做用域链一级一级的操做,查找顺序是从当前变量对象开始,知道找到为止,若是找不到就会一般会有异常
var color = "blue";
function changeColor() {
var otherColor = "red";
function swapColor() {
var tempColor = otherColor;
otherColor = color;
color = tempColor;
// 这里能够访问 tempColor otherColor color
}
swapColor();
// 这里能够访问 otherColor color swapColor
}
changeColor();
// 这里能够访问 changeColor color
复制代码
看图
下面代码中你们要理解函数的多面性,多个身份
接下来说用到函数的是两个身份普通函数、普通对象, 看代码()
function foo(){
this.count++
}
var count=0;
foo.count=0;
for(var i=0;i<5;i++){
foo()
}
console.log(foo.count)//0
console.log(count)//5
复制代码
从打印的结果上来看显然,this指向的不是自己函数,固然我们通常看到这类的问题我们就会绕道而行,看代码
function foo(){
this.count++
}
var bar={
count:0
}
foo.count=0;
for(var i=0;i<5;i++){
foo.call(bar)
}
console.log(bar.count)//5
console.log(count)//0
复制代码
虽然这种解决方案很好,也会有其余的解决方案,可是咱们仍是不理解this的问题,内心仍是有种不安之感
接下来说用到函数的是两个身份普通函数、普通对象, 看代码()
function foo(){
var num=2;
console.log(this.num)
}
var num=0;
foo()//0
复制代码
我们看到代码的执行结果后,发现this指向的并非该函数的做用域。
图中我们看到this是在函数执行的时候建立的。
前面几步我们已经肯定的this的建立和this的指向的误区,接下啦我们要看看this的绑定的规则,分为4个规则。
function foo(){
var num=2;
this.num++
console.log(this.num)
}
var num=0;
foo()//1
复制代码
上面代码中就实现了默认绑定,在foo方法的代码块中操做的是window.num++。
function foo(){
console.log(this.name)
}
var bar={
name:'shiny',
foo:foo
}
bar.foo()//shiny
复制代码
要须要补充一点,无论你的对象嵌套多深,this只会绑定为直接引用该函数的地址属性的对象,看代码
function foo(){
console.log(this.name)
}
var shiny={
name:'shiny',
foo:foo
}
var red={
name:'red',
obj:shiny
}
red.obj.foo()//shiny
复制代码
function foo(){
console.log(this.name)
}
var shiny={
name:'shiny',
foo:foo
}
function doFoo(fn){
fn()
}
doFoo(shiny.foo)//undefind
复制代码
你们知道函数参数在函数执行的时候,其实有一个赋值的操做,我来解释一下上面的,当函数doFoo执行的时候会开辟一个新的栈并被推入到全局栈中执行,在执行的过程当中会建立一个活动对象,这个活动对象会被赋值传入的参数以及在函数中定义的变量函数,在函数执行时用到的变量和函数直接从该活动对象上面取值使用。 看图 doFoo的执行栈
fn的执行栈
看下面原理和上面同样经过赋值,致使隐式绑定的丢失,看代码
function foo(){
console.log(this.name)
}
var shiny={
name:'shiny',
foo:foo
}
var bar = shiny.foo
bar()//undefined
复制代码
你们是否是已经明白了为何是undefined,来解释一波,其实shiny的foo属性是引用了foo函数的引用内存地址,那么有把foo的引用地址赋值给了 bar 那么如今的bar的引用地址个shiny.foo的引用地址是一个,那么执行bar的时候也会触发默认绑定规则由于没有其余规则能够匹配,bar函数执行时,函数内部的this绑定的是全局变量。
看下满的引用地址赋值是出现的,奇葩 隐式绑定丢失,看代码
function foo(){
console.log(this.name)
}
var shiny={
name:'shiny',
foo:foo
}
var red={
name:'red'
}
(red.foo=shiny.foo)()//undefined
复制代码
赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,所以调用位置是 foo() 而不是 p.foo() 或者 o.foo()。根据咱们以前说过的,这里会应用默认绑定。
看代码
function foo(){
console.log(this.age)
}
var shiny={
age:20
}
foo.call(shiny)//20
function bar(){
console.log(this.age)
}
var red={
age:18
}
bar.apply(red)//18
复制代码
这两个方法都是显式的绑定了tihs
function foo(b){
return this.a+b
}
var obj={
a:2
}
function bind(fn,obj){
return function(){
return fn.apply(obj,arguments)
}
}
bind(foo,obj)(3)//5
复制代码
语言解释: 经过apply + 闭包机制 实现bind方法,实现强行绑定规则
API调用的“上下文” 第三方库或者寄生在环境,以及js内置的一些方法都提供了一下 content 上下文参数,他的做用和 bind同样,就是确保回调函数的this被绑定
function foo (el){
console.log(el,this.id)
}
var obj ={
id:'some one'
};
[1,2,4].forEach(foo,obj)
// 1 some one 2 some one 4 some one
复制代码
传统面向类的语言中的构函数,是在使用new操做符实例化类的时候,会调用类中的一些特殊方法(构造函数)
不少人认为js中的new操做符和传统面向类语言的构造函数是同样的,其实有很大的差异
重新认识一下js中的构造函数,js中的构造函数 在被new操做符调用时,这个构造函数不属于每一个类,也不会创造一个类,它就是一个函数,只是被new操做符调用。
使用new操做符调用 构造函数时会执行4步
我们了解了js new 操做符调用构造函数时都作了些什么,哪么我们就知道构造函数里面的this是谁了
代码实现
function Foo(a){
this.a=a
}
var F = new Foo(2)
console.log(F.a)//2
复制代码
看代码
function foo(){
console.log(this.name)
}
var shiny={
name:'shiny',
foo:foo
}
var red={
name:'red'
}
shiny.foo()//shiny
shiny.foo.call(red)// red
shiny.foo.apply(red)// red
shiny.foo.bind(red)()//red
复制代码
显然在这场绑定this比赛中,显式绑定赢了隐式绑定
function foo(name){
this.name=name
}
var shiny={
foo:foo
}
shiny.foo('shiny')
console.log(shiny.name)//shiny
var red = new shiny.foo('red')
console.log(red.name)//red
复制代码
显然在这场绑定this比赛中new 操做符绑定赢了隐式绑定
使用call、apply方法不能结合new操做符会报错误
可是我们能够是bind绑定this来比较 显式绑定和new操做符的绑定this优先级。 看代码
function foo(){
console.log(this.name)
}
var shiny={
name:'shiny'
}
var bar = foo.bind(shiny)
var obj = new bar();
console.log(obj.name)// undefind
复制代码
显然 new操做符绑定 打败了 显式绑定
foo.call(window)
obj.foo();
function foo(){
console.log(name)
}
var name ='shiny'
foo.call(null)//shiny
foo.call(undefined)//shiny
var bar = foo.bind(null)
var baz = foo.bind(undefined)
bar()//siny
baz()//siny
复制代码
把 null、undefined经过 apply、call、bind 显式绑定,虽然实现可默认绑定,可是建议这么作由于在非严格的模式下会给全局对象添加属性,有时候会形成不可必要的bug。
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 咱们的空对象
var ø = Object.create( null );
// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3
复制代码
function foo(){
return ()=>{
console.log(this.name)
}
}
var obj ={
name:'obj'
}
var shiny ={
name:'shiny'
}
var bar = foo.call(obj);
bar.call(shiny)// foo
复制代码
咱们看到箭头函数的this被绑定到该函数执行的做用域上。
我们在看看 js内部提供内置函数使用箭头函数
function foo() {
setTimeout(() => {
// 这里的 this 在此法上继承自 foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2
复制代码
箭头函数能够像 bind(..) 同样确保函数的 this 被绑定到指定对象,此外,其重要性还体 如今它用更常见的词法做用域取代了传统的 this 机制。实际上,在 ES6 以前咱们就已经 在使用一种几乎和箭头函数彻底同样的模式。
function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
复制代码
虽然 self = this 和箭头函数看起来均可以取代 bind(..),可是从本质上来讲,它们想替 代的是 this 机制。 若是你常常编写 this 风格的代码,可是绝大部分时候都会使用 self = this 或者箭头函数。 若是彻底采用 this 风格,在必要时使用 bind(..),尽可能避免使用 self = this 和箭头函数。
在函数执行过程当中,为读取和写入变量的值,就须要在做用域链中查找变量。来看下面的例子。
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
复制代码
以上代码先定义了 compare()函数,而后又在全局做用域中调用了它。当调用 compare()时,会 建立一个包含 arguments、value1 和 value2 的活动对象。全局执行环境的变量对象(包含 result 和 compare)在 compare()执行环境的做用域链中则处于第二位。图片 展现了包含上述关系的 compare()函数执行时的做用域链。
后台的每一个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像 compare()函数这样的局部环境的变量对象,则只在函数执行的过程当中存在。在建立 compare()函数 时,会建立一个预先包含全局变量对象的做用域链,这个做用域链被保存在内部的[[Scope]]属性中。 当调用 compare()函数时,会为函数建立一个执行环境,而后经过复制函数的[[Scope]]属性中的对 象构建起执行环境的做用域链。此后,又有一个活动对象(在此做为变量对象使用)被建立并被推入执 行环境做用域链的前端。对于这个例子中 compare()函数的执行环境而言,其做用域链中包含两个变 量对象:本地活动对象和全局变量对象。显然,做用域链本质上是一个指向变量对象的指针列表,它只 引用但不实际包含变量对象。 不管何时在函数中访问一个变量时,就会从做用域链中搜索具备相应名字的变量。通常来说, 当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局做用域(全局执行环境的变量对象)。 可是,闭包的状况又有所不一样。
在看一个案例
function createComparisonFunction(propertyName) {
return function (object1, object2) {
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
复制代码
在这个例子中,object1[propertyName] object2[propertyName] 两行代码是内部函数(一个匿名函数)中的代码,这两行代码访问了外部 函数中的变量 propertyName。即便这个内部函数被返回了,并且是在其余地方被调用了,但它仍然可 以访问变量 propertyName。之因此还可以访问这个变量,是由于内部函数的做用域链中包含 createComparisonFunction()的做用域。要完全搞清楚其中的细节,必须从理解函数被调用的时候 都会发生什么入手。
当某个函数被调用时,会建立一个执行环境(execution context)及相应的做用域链。 而后,使用 arguments 和其余命名参数的值来初始化函数的活动对象(activation object)。但在做用域 链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至做为做用域链终点的全局执行环境
看图
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function () {
return i;
};
}
return result;
}
复制代码
这个函数会返回一个函数数组。表面上看,彷佛每一个函数都应该返本身的索引值,即位置 0 的函数 返回 0,位置 1 的函数返回 1,以此类推。但实际上,每一个函数都返回 10。由于每一个函数的做用域链中 都保存着 createFunctions() 函数的活动对象,因此它们引用的都是同一个变量 i 。 当 createFunctions()函数返回后,变量 i 的值是 10,此时每一个函数都引用着保存变量 i 的同一个变量 对象,因此在每一个函数内部 i 的值都是 10。可是,咱们能够经过建立另外一个匿名函数强制让闭包的行为 符合预期,以下所示。
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = (function (num) {
return function () {
return num;
};
})(i);
}
return result;
}
复制代码
在重写了前面的 createFunctions()函数后,每一个函数就会返回各自不一样的索引值了。在这个版 本中,咱们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将当即执行该匿名函数的结果赋 给数组。这里的匿名函数有一个参数 num,也就是最终的函数要返回的值。在调用每一个匿名函数时,我 们传入了变量 i。因为函数参数是按值传递的,因此就会将变量 i 的当前值复制给参数 num。而在这个 匿名函数内部,又建立并返回了一个访问 num 的闭包。这样一来,result 数组中的每一个函数都有本身 num 变量的一个副本,所以就能够返回各自不一样的数值了。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
return function () {
return this.name;
};
},
};
alert(object.getNameFunc()()); //"The Window"
复制代码
每一个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函 数在搜索这两个变量时,只会搜索到其活动对象为止,所以永远不可能直接访问外部函数中的这两个变 量。不过,把外部做用域中的 this 对象保存在一个闭包可以访问 到的变量里,就可让闭包访问该对象了。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
var that = this;
return function () {
return that.name;
};
},
};
alert(object.getNameFunc()()); //"My Object"
复制代码
在几种特殊状况下,this 的值可能会意外地改变。好比,下面的代码是修改前面例子的结果。
var name = "The Window";
var object = {
name: "My Object",
getName: function () {
return this.name;
},
};
复制代码
第一行代码跟日常同样调用了 object.getName(),返回的是"My Object",由于 this.name 就是 object.name。第二行代码在调用这个方法前先给它加上了括号。虽然加上括号以后,就好像只 是在引用一个函数,但 this 的值获得了维持,由于 object.getName 和(object.getName)的定义 是相同的。第三行代码先执行了一条赋值语句,而后再调用赋值后的结果。由于这个赋值表达式的值是 函数自己,因此 this 的值不能获得维持,结果就返回了"The Window"。 固然,你不大可能会像第二行和第三行代码同样调用这个方法。不过,这个例子有助于说明即便是 语法的细微变化,都有可能意外改变 this 的值。
浅拷贝:
建立一个新对象,这个对象有着原始对象属性值的一份精确拷贝。若是属性是基本类型,拷贝的就是基本类型的值,若是属性是引用类型,拷贝的就是内存地址 ,因此若是其中一个对象改变了这个地址,就会影响到另外一个对象。
深拷贝:
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
JSON.parse(JSON.stringify());
复制代码
这种写法很是简单,并且能够应对大部分的应用场景,可是它仍是有很大缺陷的,好比拷贝其余引用类型、拷贝函数、循环引用等状况。
function clone(target) {
if (typeof target === 'object') {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
复制代码
这是一个最基础版本的深拷贝,这段代码可让你向面试官展现你能够用递归解决问题,可是显然,他还有很是多的缺陷,好比,尚未考虑数组。
在上面的版本中,咱们的初始化结果只考虑了普通的object
,下面咱们只须要把初始化代码稍微一变,就能够兼容数组了:
module.exports = function clone(target) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
};
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
复制代码
OK,没有问题,你的代码又向合格迈进了一小步。
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
target.target = target;
复制代码
很明显,由于递归进入死循环致使栈内存溢出了。
缘由就是上面的对象存在循环引用的状况,即对象的属性间接或直接的引用了自身的状况:
解决循环引用问题,咱们能够额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当须要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,若是有的话直接返回,若是没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
这个存储空间,须要能够存储key-value
形式的数据,且key
能够是一个引用类型,咱们能够选择Map
这种数据结构:
map
中有无克隆过的对象key
,克隆对象做为value
进行存储function clone(target, map = new Map()) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
for (const key in target) {
cloneTarget[key] = clone(target[key], map);
}
return cloneTarget;
} else {
return target;
}
};
复制代码
接下来,咱们可使用,WeakMap
提代Map
来使代码达到画龙点睛的做用。
function clone(target, map = new WeakMap()) {
// ...
};
复制代码
为何要这样作呢?,先来看看WeakMap
的做用:
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值能够是任意的。
什么是弱引用呢?
在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并所以可能在任什么时候刻被回收。
举个例子:
let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code秘密花园');
obj = null;
复制代码
虽然咱们手动将obj
,进行释放,然是target
依然对obj
存在强引用关系,因此这部份内存依然没法被释放。
再来看WeakMap
:
let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code秘密花园');
obj = null;
复制代码
若是是WeakMap
的话,target
和obj
存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。
设想一下,若是咱们要拷贝的对象很是庞大时,使用Map
会对内存形成很是大的额外消耗,并且咱们须要手动清除Map
的属性才能释放这块内存,而WeakMap
会帮咱们巧妙化解这个问题。