深入理解Javascript中的原型和闭包

这两天看了很早前的一个原型和闭包系列,来自于深入理解Javascript中的原型和闭包系列,对里面十几篇文章做了一些记录,当然里面也有我的一些理解,但基本都是从该系列中提取的一些认为比较重要的点。

  • 一切(引用类型)都是对象,JS引用类型包括数组、对象、null、函数、new Number(10),他们都是对象。判断一个变量是否为对象十分简单,值类型的判断用typeof,而引用类型的判断可以用instanceof。[其中值类型包括undefined,number,string,boolean]

    1
    2
    var fn = new Function();
    console.log(fn instanceof Object); //true
  • 对象——若干属性的集合,对象里面的一切都是属性,只有属性,没有方法。方法如何表示?方法也是一种属性,它的属性表示为键值对的形式。

1
2
3
4
5
6
7
8
var obj = {
a: 1,
b: (x) => { alert(this.a + x); },
c: {
name: "Lcina",
age: 22
}
}
  • 对象都是通过函数创建的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Test() {
this.name = "Lcina"
}
var t = new Test();

var obj = {a: 1, b: 2}; 等价于
var obj = new Object();
obj.a = 1;
obj.b = 2;
console.log(obj); // {a: 1, b: 2}

var array = [1, 2, 3]; 等价于
var arr = new Array[];
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
  • 每个函数都有一个属性叫做protopyte。
1
2
3
4
5
6
7
8
function Fn() { }
Fn.prototype.name = 'Lcina';
Fn.prototype.getYear = function () {
return 200009871;
};
var fn = new Fn();
console.log(fn.name); // Lcina
console.log(fn.getYear()); // 200009871
  • 每个对象都有一个proto属性,指向创建该对象的函数的prototype
1
2
var obj = {};
console.log(obj.__proto__);



从控制台输出可以看出obj.__proto__Object.prototype的属性一样,即obj.__proto__ === Object.prototype 上面可以看出两者属性是一样的,也就是结果为true。由控制台输出信息可以看出obj.__proto__的结果是一个对象,那么Object.prototype自然而然也是一个对象,由于每个对象都有一个__proto__属性,那么Object.prototype__proto__指向哪? 其中obj是由Object创建的,所以它的__proto__指向Object.prototype。而Object.prototype确实是一个特例,它的proto`指向的是null,控制台输出可以告诉你答案。

1
2
function Foo() {...}
var f1 = new Foo();

f1 –> f1.__proto__ <=> Foo.prototype(.__proto__) <=> Function.prototype(.__proto__) <=> Object.prototype;
其中f1 instanceof Function的结果为false,对象是由函数创建的,f1是函数创建的一个对象,并不等同于Function。

instanceof表示的就是一种继承关系,或者原型链的结构。

  • Javascript中的继承是通过原型链来体现的。
1
2
3
4
5
6
7
function Foo() {}
var f1 = new Foo();
f1.a = 10;
Foo.prototype.a = 100;
Foo.prototype.b = 200;
console.log(f1.a); // 10
console.log(f1.b); // 200


f1由函数Foo创建得来的对象,控制台输出f1.a的值为10,是因为f1.a是f1的基本属性,f1.b是怎么得来的?从Foo.prototype得来,由于f1.__proto__指向Foo.prototype,所以f1.b的值为200。访问一个对象的属性时,先在基本属性中查找,如果没有,在沿着proto这条链向上找,这就是原型链。 实际开发应用如何区分一个属性到底是基本属性还是从原型中得到的[特别在for…in…循环中需要注意]?这里需要用到hasOwnProperty。

1
2
3
4
5
6
7
8
9
var item;
for(item in f1) {
console.log(item); // a b
}
for(item in f1) {
if(f1.hasOwnProperty(item)) {
console.log(item); // a
}
}

f1 –> f1.__proto__ <=> Foo.prototype(.__proto__) <=> Function.prototype(.__proto__) <=> Object.prototype;
对象f1的hasOwnProperty()方法由Object.prototype继承得来,可以看上述的指向,说明Function.prototype中同样继承来自Object原型中的hasOwnProperty()方法。

  • 执行上下文
1
2
3
4
5
6
7
8
// 情况1
console.log(a); // Uncaught ReferenceError: a is not defined
// 情况2
console.log(a); // undefined
var a;
// 情况3
console.log(a); //undefined
var a = 10;

针对情况3,在一句一句执行js代码之前,浏览器已经做了一些“准备工作”,其中就包括对变量的声明,但不包括赋值,所以情况3实际的执行代码步骤应该是这样:var a; console.log(a); a = 10;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 函数表达式和函数声明

// 情况1[函数声明]
console.log(f1); // function f1() {...}
function f1() {...}

// 情况2[函数表达式]
console.log(f2); // undefined
var f2 = function() {...};

/*
* 总结:
* 1. 变量、函数表达式——变量声明,默认赋值为undefined
* 2. this——赋值
* 3. 函数声明——赋值
*/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
var i = 2;
if(i > 1) {
var name = "Lcina";
}
console.log(name); // Lcina

// JavaScript除了全局作用域之外,只有函数可以创建的作用域。
// 在声明变量时,全局代码要在代码前面声明;
// 而函数中使用到的变量要在函数体一开始声明。其他地方最好不要出现变量声明。
// 作用域的最大用处是隔离变量,不同作用域下的同名变量不会有冲突。

function fn1() { // fn1作用域
var a = 1;
function fn2() { // fn2作用域
var a = 2;
}
}

// http://www.cnblogs.com/wangfupeng1988/p/3991995.html

// 作用域情况1
var a = 10, b = 20;
function fn(x) {
var a = x;
console.log( {fn: a} ); // 10
function bar (x) {
var a = x;
console.log( {bar: a} ); // 100
}
bar(100);
}
fn(10);

// 作用域情况2
var a = 10, b = 20;
function fn(x) {
var a = x;
console.log( {fn: a} ); // 10
function bar () {
var a = x;
console.log( {bar: a} ); // 10
}
}
fn(10);

// 通过作用域对应的执行上下文环境来获取变量的值。
// 作用域中变量的值是在执行过程中产生确定的,
// 而作用域确是在函数创建时就确定的。

// “自由变量”到“作用链”情况1
var a = 10;
function fn(){
var b = 20;
function bar() {
console.log(a+b);
}
return bar;
}
var x = fn(),
b = 200;
x(); // 30


1
2
3
4
5
6
7
8
9
// 要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”
/*
* 在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。比如上图中的a就是一个自由变量。
* 第一步,现在当前作用域查找a,如果有则获取并结束。如果没有则继续;
* 第二步,如果当前作用域是全局作用域,则证明a未定义,结束;否则继续;
* 第三步,(不是全局作用域,那就是函数作用域)将创建该函数的作用域作为当前作用域;
* 第四步,跳转到第一步。
* 根据上面的例子可以这样理解[作者应该估计也是这么理解的]:根据函数fn创建了对象x,调用x(),fn函数体里面可以直接看到的就是输出到控制台的内容是什么?执行函数fn的结果是返回bar,即控制台输出的a+b的值。那么函数bar()中的a的值,从当前作用域出发,最终发现不管是bar作用域还是fn作用域都没有定义,最终找到了自由变量a,所以a的值为10。那么b的值,并不存在自由变量b,所以b的值可以到创建函数bar的fn作用域获取,也就是作者提到的第三步,得到b的值为20,所以后面即使给b赋值为200,其实也没用,所以最终结果为30。如果将fn作用域中的b定义拿掉,那最终结果就有210,这里不解释了。注意:后面虽然重新定义了b的值为200,但不影响作用域中b的值,本身两者作用域都不同,也就是这两个变量半毛钱关系没有,只是两个命名的名字一样而已。
*/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// “自由变量”到“作用链”情况2
var a = 10;
var b = 100;
function fn(){
var b = 20;
function bar() {
console.log(a+b);
}
return bar;
}
var x = fn(),
b = 200;
x(); // 30

/*
* 对之前代码进行了修改,定义了自由变量b=100,执行结果仍为30。
* 因为在fn作用域找到了b已经定义,所以bar作用域中的b的值来自于fn作用域,
* 即b的值仍为20。
*/

// “自由变量”到“作用链”情况3
var a = 10;
var b = 100;
function fn(){
function bar() {
console.log(a+b);
}
return bar;
}
fn()(); // 110
var x = fn(),
b = 200;
x(); // 210

/*
* 这种情况发现不管是fn/bar作用域,都不存在b这个变量,
* 所以就找到了自由(全局)变量b=100,但在创建对象x后,
* 全局变量b被重新定义b=200,所以当x()时,最终的结果为210。
*/

var max = 10,
fn = function (x) {
if (x > max) {
console.log(x);
}
};
(function (f) {
var max = 100;
f(15);
})(fn) // 15
// 这里的max为10,而不是100,注意max,fn都为变量,
// 那么fn被赋予的方法中的max值应该来源与自由变量max=10,
// 而不是(function(f){})()中的max值,具体可以参考上面的例子解释。