原型闭包作用域

作用域

查找变量时,一层一层由内向外查找,一旦找到第一个匹配就停止查找。

当相同的变量在多个层中声明时,内层的变量会遮蔽外层。

欺骗词法作用域:

eval:在非strict模式下,eval会运行js代码,改变作用域;在strict模式下,知识运行代码不会修改作用域。

函数提升:

函数声明还会比变量先提升,当有多个声明,后续声明会覆盖前面的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
foo(); // 3
foo = function () {
console.log('1');
}
function foo() {
console.log('2');
}
function foo() {
console.log('3');
}

执行时:

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
console.log('2');
} // 函数声明优先提升
function foo() {
console.log('3');
} // 后续声明覆盖前面的声明
foo();
foo = function () {
console.log('1');
}

闭包

闭包就是函数能够记住并访问他们的词法作用域,即使当这个函数在它的词法作用域之外执行。

循环+闭包(*)

用来展示闭包最常见最权威的例子是老实巴交的 for 循环。

1
2
3
4
5
for (var i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}

当执行上述代码,会发现每隔一秒会输出一个6。

setTimeout下的循环和闭包(自己补充部分)

对于上述代码的理解:结合异步闭包作用域的知识。

对setTimeout的理解

首先要理清对setTimeout的理解:

setTimeout的延迟不是绝对精确的;
setTimeout的意思是传递一个函数,延迟一段时候把该函数添加到队列当中,并不是立即执行;
所以说如果当前正在运行的代码没有运行完,即使延迟的时间已经过完,该函数会等待到函数队列中前面所有的函数运行完毕之后才会运行;

也就是说所有传递给setTimeout的回调方法都会在整个环境下的所有代码运行完毕之后执行。

比如:

1
2
3
4
5
6
7
8
9
setTimeout(function(){
console.log("here");
}, 2000);
var i = 0;
//具体数值根据你的计算机CPU来决定,达到延迟效果就好
while (i < 300000000) {
i ++;
}
console.log("test");

上面的代码运行时,会先输出test,2秒后再输出here。

为什么是一秒

因为根据上述的理解,setTimeout会在for循环结束后运行,所以队列中会依次加入延迟1秒、2秒、3秒…的setTimeout函数,所以在运行的时候延迟1秒的会先运行,然后对于延迟2秒的在延迟1秒的setTimeout运行的同时也跟着延迟了1秒,所以当延迟1秒的setTimeout运行结束后,只需要延迟1秒就可以开始运行函数。 延迟3秒、4秒…的同理。

为什么每次都显示6

可以将循环转化为:

1
2
var i;
for(i = 1; i <= 5; i++) {...}

首先说明对于闭包的理解:对函数类型的值进行传递时,保留对它被声明的位置所处的作用域的引用。

timer函数是在setTimeout中声明的,当运行console.log( i )时,这个itimer里面没有声明,所以向外层作用域找,这时候可以找到全局作用域上的i。当timer执行时,循环已经结束了,所以i的值为6。

改进后输出1,2,3,4,5

第一种方式:

1
2
3
4
5
6
7
8
for (var i=1; i<=5; i++) {
(function(){
var j = i;
setTimeout( function timer(){
console.log( j );
}, j*1000 );
})();
}

函数在每次迭代时,持有一个i值的拷贝。

第二种方式:

1
2
3
4
5
6
7
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer () {
console.log( j );
}, j * 1000);
})(i);
}

这种方式知识上面形式的一种改写,他们都会利用IIFE来解决这一问题,都是用立即执行函数表达式创造了新的函数作用域将timer函数包裹了起来,并用j捕获了每次循环时的i。只不过第二种方式将j作为形参,i作为实参。

第三种方式:

1
2
3
4
5
for (let i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}

let声明的变量每次都会创建一个块作用域,将上面的代码经过babel转码为ES5我们可以看到:

1
2
3
4
5
6
7
8
9
var _loop = function _loop(i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
};
for (var i = 1; i <= 5; i++) {
_loop(i);
}

它为每一次循环都创建了一个块作用域。

第四种方式:

1
2
3
4
5
6
for (var i=1; i<=5; i++) {
let j = i;
setTimeout( function timer(){
console.log( j );
}, j*1000 );
}

这种方式原理和第三种方式相同。

this

this与函数声明的位置无关,而与函数调用的位置有关,箭头函数中的this除外。

可以简化代码,不必频繁地传参。

调用点调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function baz() {
// 调用栈是: `baz`
// 我们的调用点是 global scope(全局作用域)
console.log( "baz" );
bar(); // <-- `bar` 的调用点
}
function bar() {
// 调用栈是: `baz` -> `bar`
// 我们的调用点位于 `baz`
console.log( "bar" );
foo(); // <-- `foo` 的 call-site
}
function foo() {
// 调用栈是: `baz` -> `bar` -> `foo`
// 我们的调用点位于 `bar`
console.log( "foo" );
}
baz(); // <-- `baz` 的调用点

可以在控制台中将console.trace()置于函数中查看调用栈。调用栈遵循先进后出的规则。

四种规则

默认绑定

当函数是一个直白,毫无修饰的调用时,进行默认绑定。

1、内容没在strict mode下,this指向全局变量。

2、内容在strict mode下,thisundefined

注意:这里的strict mode只有函数定义时的strict mode会产生undefined这种情况,而不是函数调用位置的strict mode影响它。

1
2
3
4
5
6
7
8
9
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: `this` is `undefined`
1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();

隐含绑定

只有对象属性引用链的最后一层是影响调用点的。

隐含丢失

1、调用点的改变

2、传递一个回调函数

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // `a` 也是一个全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

setTimeout函数的实质:

1
2
3
4
function setTimeout(fn,delay) {
// (通过某种方法)等待 `delay` 毫秒
fn(); // <-- 调用点!
}

明确绑定

利用call()apply()bind实现的绑定。

new绑定

当在函数前面加入new调用,即构造器调用时,会发生下面几件事:

1、创建一个全新的对象。

2、新构建的对象会被接入原型链。

3、这个新创建的函数会被设置为函数调用的this绑定。

一切皆有顺序

明确绑定优先于隐含绑定

明确绑定优先于new绑定

有一种情况可以让new绑定覆盖明确绑定:

1
2
3
4
5
6
7
8
9
10
11
12
function foo(p1,p2) {
this.val = p1 + p2;
}
// 在这里使用 `null` 是因为在这种场景下我们不关心 `this` 的硬绑定
// 而且反正它将会被 `new` 调用覆盖掉!
// 这种方式称为bind的柯里化
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2

总结

1、默认绑定:当函数被直白毫无修饰地调用时就为默认绑定。

2、隐含绑定:函数是通过对象调用的;

3、明确绑定:函数通过callapplybind调用;

4、new绑定:通过new调用。

特例

如果你传递 nullundefined 作为 callapplybindthis 绑定参数,那么这些值会被忽略掉,取而代之的是 默认绑定 规则将适用于这个调用。

利用这种方式来展开一个数组,作为函数调用的参数;

利用bind()来柯里化参数,即增加预设值。

1
2
3
4
5
6
7
8
9
10
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 将数组散开作为参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 用 `bind(..)` 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

对象

js的七种基本数据类型:

null、undefined、number、string、object、boolean、symbol

js中的九种内建对象:(它们每一个都可以被当做构造器)

String、Number、Boolean、Object、Function、Array、Date、RegExp、Error

属性描述符

定义属性描述:

1
2
3
4
5
6
7
8
9
10
var myObject = {};
Object.defineProperty(myObject, 'a', {
value: 2,
writable: true,
enumerable: true,
configurable: true // 该设置是一个单向操作,一旦设置不能更改
});
myObject.a; // 2

存在性

1
2
3
4
5
6
7
8
9
var myObject = {
a: 2
};
'a' in myObject; // true 检查属性是否在对象中
myObject.hasOwnProperty('a'); // true 仅检查myObject,不检查它的原型链
更健壮的方法:
Object.prototype.hasOwnProperty.call(myObject, 'a');

迭代

forEach(...):迭代数组中的值;

for...of:用来迭代数组和有迭代器的对象;

手写迭代器:
1
2
3
4
5
6
7
8
9
10
11
function myIterator(myArray) {
var nextIndex = 0;
return {
next: function (){
return nextIndex < myArray.length?
{value: myArray[nextIndex], done: false}:
{done: true}
}
}
}

迭代器其实就是用可以用next调用其中的值,直到没有值可以调用,这是done返回true

原型

设置与遮蔽属性

1
myObject.foo = "bar";

进行上述操作会出现四种情况:

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