深入理解js原型和闭包笔记

一切都是对象

对象就是属性的集合。在js里对象里只有属性没有方法,因为方法也是一种属性


函数和对象的关系

对象都是通过函数创建的,而函数又是一种对象


prototype原型

1
2
3
4
5
6
7
function Fn() { }
Fn.prototype.name = 'Joie';
Fn.prototype.getYear = function () {
return 1997;
};

var fn = new Fn();

上面代码片段中fn.__proto__ === Fn.prototype也就是说实例化对象的隐式原型全等于创建这个对象的函数的显示原型


隐式原型

每个对象都有一个显式原型和隐式原型,函数也是。同时每个对象的__proto__属性,指向创建该对象的函数的prototype。

  • 但是Object.prototype是一个特例,它的__proto__指向的是null!!!

  • Function.__proto__指向Function.prototype形成一个循环


instanceof

对于值类型的判断可以用typeof,但是对于引用类型的判断需要instanceof。

instanceof的判断规则:沿着A的__proto__这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true,如果找到终点还未重合,则返回false。


原型链与继承

1
2
3
4
5
6
7
8
9
10
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

访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__这条链向上找,这就是原型链。

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

如果要区分一个属性到底是基本的还是从原型中找到的,可以用hasOwnProperty特别是在for...in...循环中

但是f1的hasOwnProperty方法是从哪里来的呢?f1本身没有,Foo.prototype中也没有。不难发现其实它是从Object.prototype中来的。

  • 由于所有的对象的原型链都会找到Object.prototype,因此所有的对象都会有Object.prototype的方法。这就是所谓的继承

  • 再例如所有函数都有callapply方法,都有lengthargumentscaller等属性。为什么会有呢,就是因为继承。函数是由Function函数创建的,因此继承的Function.prototype中的方法。


原型的灵活

1
2
3
4
5
var obj = {a:10 , b:20}
console.log(obj.toString()); // [object Object]

var arr = [1, 2, true];
console.log(arr.toString()); //1, 2, true
  • 继承的方法不合适,可以做出修改。如上Object和array的toString()方法不一样,肯定是Array.prototype.toString()方法做了修改。

  • 内置方法的原型属性可以手动添加和自定义,所以在添加之前最好先做一次判断,判断这个属性是否本来就存在。


执行上下文

1
2
3
4
5
6
7
console.log(a);  // a is not defined

console.log(a); // undefined
var a;

console.log(a); // undefined
var a = 10;
  • 这是因为浏览器的预解析的机制,当预解析到var的时候,默认给一个undefined,执行的时候才会去读赋的值。
1
console.log(this); // Window{top: Window, window:...}
  • 无论在那个位置获取this都是有值得。但是this和变量不同,在预解析的时候变量之声明不赋值,但是this会直接赋值。
1
2
3
4
5
console.log(f1); // function f1(){}
function f1(){}; //函数声明

console.log(f2); // undefined
var f2 = function(){}; //函数表达式
  • 对于函数,对待函数表达式就像变量一样,对待函数声明时,却把整个函数赋值了。

  • 函数每被调用一次,都会产生一个新的执行上下文环境,因为不同的调用可能会有不同的参数。

  • 且函数在定义的时候(不是在调用的时候),就已经确定了函数体内部自由变量的作用域。

总结

  • 变量、函数表达式 — 变量声明默认赋值undefined
  • this — 赋值
  • 函数声明 — 赋值
  • 如果代码段是函数体,那么arguments和自由变量的取值作用域都是赋值

这三种数据的准备情况被称之为“执行上下文”或者“执行上下文环境”

执行上下文环境的通俗定义就是:在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空

this

在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了。因为this的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境

  • 构造函数

所谓构造函数就是用来new一个对象的函数。严格来讲所有的函数都可以new一个对象,但是有些函数的定义就是为了去new一个对象,而有些函数则不是。PS: 构造函数的函数名第一个字母大写(规则约定)。如:Object、Array、Function等等。

1
2
3
4
5
6
7
8
function Foo(){
this.name = "Joie";
this.year = 1997;

console.log(this); // Foo name: "Joie" year: 1997 __proto__: Object
}

var f1 = new Foo();

那么在构造函数的prototype中,this代表什么

1
2
3
4
5
6
7
8
9
10
11
function Fn(){
this.name = "Joie";
this.year = 1997;
}

Fn.prototype.getName = function(){
console.log(this.name);
}

var f1 = new Fn();
f1.getName(); // Joie

可以在上面的代码中看出来this指向了f1这个对象。
其实,不仅仅是构造函数prototype,即便是在整个原型链中,this代表的也都是当前对象的值。

1
2
3
4
5
6
7
8
function Foo(){
this.name = "Joie";
this.year = 1997;

console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
}

Foo();

由上面的代码可以看出来,当Foo函数作为构造函数的时候,其中的this就代表它即将new出来的对象。
而直接调用Foo函数时,其中的this就是window

  • 函数作为对象的一个属性

如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数中的this指向该对象。而不作为函数的属性被调用那么this的值就会之window

1
2
3
4
5
6
7
8
9
var obj = {
x : 10,
fn : function(){
console.log(this); // {x: 10, fn: ƒ} fn: ƒ () x: 10 __proto__: Object
console.log(this.x); // 10
}
}

obj.fn();
1
2
3
4
5
6
7
8
9
10
var obj = {
x : 10,
fn : function(){
console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
console.log(this.x); // undefined
}
}

var f1 = obj.fn;
f1();
  • 函数用call或者apply调用

当函数被call和apply调用时,this的值就取传入的对象的值。

1
2
3
4
5
6
7
8
9
10
var obj = {
x : 10
}

function fn(){
console.log(this); // {x: 10} x: 10 __proto__: Object
console.log(this.x); // 10
}

fn.call(obj);
  • 全局和普通调用函数

在全局环境下,this永远是window

1
console.log(this === window); // true

普通函数在调用的时候其中的this也都是window,但也有需要注意的情况

1
2
3
4
5
6
7
8
var x = 10;

function fn(){
console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
console.log(this.x); // 10
}

fn();
1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
x : 10,
fn : function(){
function f1(){
console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
console.log(this.x); // undefined
}
f1();
}
}

obj.fn();

执行上下文栈

执行上下文栈就是一个压栈和出栈的过程,流程如图:

执行全局代码时,会产生一个执行上下文环境,每次调用函数又会产生执行上下文环境。当函数完成时,这个上下文环境以及其中的数据都会被销毁,再重新回到全局上下文环境。

具体参考博客–深入理解javascript原型和闭包(11)——执行上下文栈


作用域

es6之前JavaScript没有块级作用域,JavaScript除了全局作用域之外,只有函数可以创建作用域。是因为var的变量提升,在if语句或者for()括号内定义的变量在块外部也可以访问到。但在es6之后用letconst声明变量就只在所在的块有效了。所谓的块就是{}大括号里的语句。

  • 作用域是一个抽象概念类似于一个地盘

  • 作用域有上下级关系,上下级关系确定就看是在哪个作用域下创建的

  • 作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。


作用域和上下文环境

  • 作用域在函数定义时就已经确定了。而不是在函数调用时确定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = 10,
b = 20;

function fn (x){
var a = 100,
b = 300;

function br (x){
var a = 1000,
b = 4000;
}
br(100);
br(200);
}

fn(10);
  • 整个执行流程:确定全局上下文环境 -> 在调用fn时,生成fn函数的上下文环境 -> 压栈 -> 并将此上下文环境设为活动状态 -> 执行完之后销毁上下文环境(调用br的时候,也是类似流程)

  • 作用域只是一个抽象感念,类似于地盘,其中没有变量。变量要通过作用域对应的执行上下文环境来获得变量的取值。

  • 作用域中变量的值是在执行过程中产生和确定的,而作用于是在函数创建的时候就确定了。

  • 如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。


从自由变量到作用域链

1
2
3
4
5
var x = 10;
function fn(){
var b = 20;
console.log(x); //x在这里就是一个自由变量
}
  • b的值是可以直接在fn的作用域中去取的,因为b就是在这里定义的。x的取值就需要到另一个作用域中去取。

  • 具体是那个作用域呢,不应该描述为父作用域,有些情况下会产生歧义。应该是到创建这个函数的那个作用域中去取值,而不是调用这个函数的作用域去取值。这就是所谓的静态作用域

  • 上段代码描述的只是跨一步作用域去寻找,如果跨一步没有找到就接着跨,一直跨到全局作用域为止。要是全局作用域都没有找到,那就真的没有了。而这一步步跨域的“路线”就是我们所说的作用域链。

取自由变量时的“作用域链”过程:

  1. 在当前作用域查找a,如果有则获取并结束,如果没有则继续。
  2. 如果当前作用域是全局作用域,则证明a未被定义并结束,否则继续。
  3. (不是全局作用域,那就是函数作用域)将创建该函数的作用域当做当前作用域。
  4. 调到第一步。

闭包

在MDN中,作者将闭包这样定义:闭包是函数和声明该函数的词法环境的组合

具体说起来有两种应用情况:

  • 函数作为返回值
1
2
3
4
5
6
7
8
9
10
function fn(){
var max = 10;
return function gn(x){
if(x > max){
console.log(x);
}
};
}
var f1 = fn();
f1(15);

上段代码在执行f1(15)时用到了fn()作用域下的变量max的值。

  • 函数作为参数被传递
1
2
3
4
5
6
7
8
9
10
11
var max = 10;
var fn = function(x){
if(x > max){
console.log(x); // 15
}
};

(function(f){
var max = 100;
f(15);
})(fn);

上段代码中,fn函数作为一个参数被传递进入另一个函数,赋值给参数f。然而在执行f的时候,变量max的取值是10而不是100。这就是因为上一小结所说的查找自由变量的取值时,是从创建这个函数的作用域去找而不是调用这个函数的作用域中去找。

在讲闭包时,除了联系作用域以外还要结合上下文执行栈。

在之前的上下执行栈中讲到有些情况下,当函数调用完之后,其执行上下文环境不会接着被销毁,这就是需要理解闭包的核心内容。

详细内容见博客

在函数返回一个函数这种闭包中,因为返回的是一个函数,函数的特殊之处在于他会创建一个独立的作用域,而他内部的变量也有可能有自由变量,因此包含返回函数的这个函数的执行上下文是不会被注销的,因为返回的函数需要顺着他之前的这个函数的执行上下文环境去获取这个自由变量。

由于在产生闭包的时候该销毁的执行上下文环境没有被销毁,所以可以很明显的看出闭包会增加内容的开销。

所以闭包和作用域及上下文环境有着密不可分的关系。


总结:

  • 上下文环境:

可以理解为一个看不见摸不着的对象(有若干个属性),虽然看不见摸不着,但确确实实存在,因为所有的变量都在里面存储着。

  • 作用域:

是抽象的。其次除了全局作用域,只有函数才能创建作用域。创建一个函数就创建了一个作用域,无论调用不调用,函数只要创建了,它就有独立的作用域,就有自己的一个“地盘”。

  • 两者对比:

一个作用域下可能有若干个上下文环境,有可能从来没有过上下文环境(函数从来没有被调用过)。有可能用过,现在函数被调用完了,上下文环境被销毁了,也有可能存在一个或多个闭包。再精简一下就是作用域是静态的组织结构,上下文是动态计算的