作用域
查找变量时,一层一层由内向外查找,一旦找到第一个匹配就停止查找。
当相同的变量在多个层中声明时,内层的变量会遮蔽外层。
欺骗词法作用域:
eval:在非strict模式下,eval会运行js代码,改变作用域;在strict模式下,知识运行代码不会修改作用域。
函数提升:
函数声明还会比变量先提升,当有多个声明,后续声明会覆盖前面的声明:
|
|
执行时:
|
|
闭包
闭包就是函数能够记住并访问他们的词法作用域,即使当这个函数在它的词法作用域之外执行。
循环+闭包(*)
用来展示闭包最常见最权威的例子是老实巴交的 for 循环。
|
|
当执行上述代码,会发现每隔一秒会输出一个6。
setTimeout下的循环和闭包(自己补充部分)
对于上述代码的理解:结合异步、闭包和作用域的知识。
对setTimeout的理解
首先要理清对setTimeout的理解:
setTimeout的延迟不是绝对精确的;
setTimeout的意思是传递一个函数,延迟一段时候把该函数添加到队列当中,并不是立即执行;
所以说如果当前正在运行的代码没有运行完,即使延迟的时间已经过完,该函数会等待到函数队列中前面所有的函数运行完毕之后才会运行;
也就是说所有传递给setTimeout的回调方法都会在整个环境下的所有代码运行完毕之后执行。
比如:
|
|
上面的代码运行时,会先输出test,2秒后再输出here。
为什么是一秒
因为根据上述的理解,setTimeout
会在for
循环结束后运行,所以队列中会依次加入延迟1秒、2秒、3秒…的setTimeout
函数,所以在运行的时候延迟1秒的会先运行,然后对于延迟2秒的在延迟1秒的setTimeout
运行的同时也跟着延迟了1秒,所以当延迟1秒的setTimeout
运行结束后,只需要延迟1秒就可以开始运行函数。 延迟3秒、4秒…的同理。
为什么每次都显示6
可以将循环转化为:
|
|
首先说明对于闭包的理解:对函数类型的值进行传递时,保留对它被声明的位置所处的作用域的引用。
timer函数是在setTimeout中声明的,当运行console.log( i )
时,这个i
在timer
里面没有声明,所以向外层作用域找,这时候可以找到全局作用域上的i
。当timer
执行时,循环已经结束了,所以i
的值为6。
改进后输出1,2,3,4,5
第一种方式:
|
|
函数在每次迭代时,持有一个i
值的拷贝。
第二种方式:
|
|
这种方式知识上面形式的一种改写,他们都会利用IIFE来解决这一问题,都是用立即执行函数表达式创造了新的函数作用域将timer函数包裹了起来,并用j捕获了每次循环时的i。只不过第二种方式将j
作为形参,i
作为实参。
第三种方式:
|
|
let
声明的变量每次都会创建一个块作用域,将上面的代码经过babel转码为ES5我们可以看到:
|
|
它为每一次循环都创建了一个块作用域。
第四种方式:
|
|
这种方式原理和第三种方式相同。
this
this
与函数声明的位置无关,而与函数调用的位置有关,箭头函数中的this
除外。
可以简化代码,不必频繁地传参。
调用点调用栈
|
|
可以在控制台中将console.trace()
置于函数中查看调用栈。调用栈遵循先进后出的规则。
四种规则
默认绑定
当函数是一个直白,毫无修饰的调用时,进行默认绑定。
1、内容没在strict mode
下,this
指向全局变量。
2、内容在strict mode
下,this
为undefined
。
注意:这里的strict mode
只有函数定义时的strict mode
会产生undefined
这种情况,而不是函数调用位置的strict mode
影响它。
|
|
|
|
隐含绑定
只有对象属性引用链的最后一层是影响调用点的。
隐含丢失
1、调用点的改变
2、传递一个回调函数
|
|
setTimeout函数的实质:
|
|
明确绑定
利用call()
、apply()
、bind
实现的绑定。
new
绑定
当在函数前面加入new
调用,即构造器调用时,会发生下面几件事:
1、创建一个全新的对象。
2、新构建的对象会被接入原型链。
3、这个新创建的函数会被设置为函数调用的this
绑定。
一切皆有顺序
明确绑定优先于隐含绑定
明确绑定优先于new
绑定
有一种情况可以让new
绑定覆盖明确绑定:
|
|
总结
1、默认绑定:当函数被直白毫无修饰地调用时就为默认绑定。
2、隐含绑定:函数是通过对象调用的;
3、明确绑定:函数通过call
、apply
、bind
调用;
4、new
绑定:通过new
调用。
特例
如果你传递 null
或 undefined
作为 call
、apply
或 bind
的 this
绑定参数,那么这些值会被忽略掉,取而代之的是 默认绑定 规则将适用于这个调用。
利用这种方式来展开一个数组,作为函数调用的参数;
利用bind()
来柯里化参数,即增加预设值。
|
|
对象
js的七种基本数据类型:
null、undefined、number、string、object、boolean、symbol
js中的九种内建对象:(它们每一个都可以被当做构造器)
String、Number、Boolean、Object、Function、Array、Date、RegExp、Error
属性描述符
定义属性描述:
|
|
存在性
|
|
迭代
forEach(...)
:迭代数组中的值;
for...of
:用来迭代数组和有迭代器的对象;
手写迭代器:
|
|
迭代器其实就是用可以用next
调用其中的值,直到没有值可以调用,这是done
返回true
。
原型
设置与遮蔽属性
|
|
进行上述操作会出现四种情况:
1、当foo
属性不直接存在于myObject
上,也不存在于myObject
的[[Prototype]]
链的更高层时。foo
作为一个新的属性被添加到myObject
上。
当 foo
不直接存在 于 myObject
,但 存在 于 myObject
的 [[Prototype]]
链的更高层时:
2、如果一个普通的名为 foo
的数据访问属性在 [[Prototype]]
链的高层某处被找到,而且没有被标记为只读(writable:false),那么一个名为 foo
的新属性就直接添加到 myObject
上,形成一个 遮蔽属性。
3、如果一个 foo
在 [[Prototype]]
链的高层某处被找到,但是它被标记为 只读(writable:false) ,那么设置既存属性和在 myObject
上创建遮蔽属性都是 不允许 的。如果代码运行在 strict mode
下,一个错误会被抛出。否则,这个设置属性值的操作会被无声地忽略。不论怎样,没有发生遮蔽。
4、如果一个 foo
在 [[Prototype]]
链的高层某处被找到,而且它是一个 setter(见第三章),那么这个 setter 总是被调用。没有 foo
会被添加到(也就是遮蔽在)myObject
上,这个 foo
setter 也不会被重定义。
如果想在第三种和第四种情况下遮蔽foo
,就不能使用=
赋值,而使用Object.defineProperty(...)
将foo
添加到myObject
。