原文:深入理解javascript原型和闭包(完结)

JavaScript 中的难点和重要点,排除知识体系之外的 bug。本篇是学习笔记,记录个人理解。

一、一切皆对象:一切(引用类型)都是对象,对象是属性的集合

  1. function show(x) {
  2.  
  3. console.log(typeof x); // undefined
  4. console.log(typeof 10); // number
  5. console.log(typeof 'abc'); // string
  6. console.log(typeof true); // boolean
  7.  
  8. console.log(typeof function () {}); //function
  9.  
  10. console.log(typeof [1, 'a', true]); //object
  11. console.log(typeof { a: 10, b: 20 }); //object
  12. console.log(typeof null); //object
  13. console.log(typeof new Number(10)); //object
  14. }
  15.  
  16. show();

  (undefined, number, string, boolean, null)属于简单的值类型,不是对象。剩下的(函数、数组、对象、new Number(10))就是引用数据类型,属于对象。

  附:   typeof xxx:判断变量的数据类型;  xxx instanceof Array:精确判断变量是否属于某一类型。

  1. var fn = function () { };
  2. console.log(fn instanceof Object); // true

  通常我们声明一个 js 对象通常写成类 json(键值对) 格式,但函数和数组也可以这样定义属性吗?——当然不行,但是它可以用另一种形式,总之函数/数组之流,只要是对象,它就是属性的集合。 以下例子:

  1. var fn = function () {
  2. alert(100);
  3. };
  4.  
  5. fn.a = 10;
  6. fn.b = function () {
  7. alert(123);
  8. };
  9. fn.c = {
  10. name: "王福朋",
  11. year: 1988
  12. };
    fn(); // 100
    fn.b; // function(){alert(123);}
    fn.b(); // 123

  上段代码中,函数就作为对象被赋值了a、b、c三个属性——很明显,这就是属性的集合吗。

  其实,jquery 中的 “$” 就是一个函数。

函数是一种对象,但是:函数和对象间的关系不简单的是父子集的关系!

详见:函 - 对 关系

  (1)对象都是通过函数创建的。

  1. function Fn() {
  2. this.name = '王福朋';
  3. this.year = 1988;
  4. }
  5. var fn1 = new Fn();

    这是最基本的创建对象的写法。这也是 JS 底层中创建对象的方法。

  (2)访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__这条链向上找,这就是原型链。(__proto__ 在“函 - 对 关系”中有相关解释)。

  1. function Foo () {};
  2. var f1 = new Foo();
  3.  
  4. f1.a = 10;
  5.  
  6. Foo.prototype.a = 100;
  7. Foo.prototype.b = 200;
  8.  
  9. console.log(f1.a); // 10
  10. console.log(f1.b); // 200

  

二、原型及原型链

  在 “一切皆对象中” 的 “函 - 对 关系”中有相应介绍。

  补充:原型链的好处:

在Java和C#中,你可以简单的理解class是一个模子,对象就是被这个模子压出来的一批一批月饼(中秋节刚过完)。压个啥样,就得是个啥样,不能随便动,动一动就坏了。

而在javascript中,就没有模子了,月饼被换成了面团,你可以捏成自己想要的样子。

首先,对象属性可以随时改动。

对象或者函数,刚开始new出来之后,可能啥属性都没有。但是你可以这会儿加一个,过一会儿在加两个,非常灵活。

三、执行上下文

  在一段js代码拿过来真正一句一句运行之前,浏览器已经做了一些“准备工作”,其中包括对变量的声明(而不是赋值。变量赋值是在赋值语句执行的时候进行的。)、this 的赋值、“函数表达式”或“函数声明”。

  • 变量、函数表达式。例如:

    1. console.log(a); // undefinded
    2. var a = 10;
    3.  
    4. console.log(f1) // undefinded
    5. var f1 = function() { };
  • 函数的声明及赋值;

    1. console.log(f1); // function f1() { }
    2. function f1() { };

    注意:“函数的声明”和“函数表达式”均发生在“准备工作”时,过程却不一样。

  以上的三种的数据准备情况称之为“执行上下文”。通俗来讲,在执行代码前,把将要用到的所有变量都事先拿出来,有的直接赋值,有的先用 undefined 占个坑。

  贴两个上下文环境的数据内容:

  • 全局代码的上下文环境:

  • 函数体中的上下文环境内容在上面的基础上多了如下内容:

  还有几点需要注意的是:

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

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

  1. var a = 10;
  2. function fn() {
  3. console.log(a);
  4. }
  5.  
  6. function bar(f) {
  7. var a = 20;
  8. f();
  9. console.log(f);
  10. }
  11.  
  12. bar(fn);
  13.  
  14. // a = 10
  15. // function fn() { console.log(a); }

  3.关于函数的执行。上例中应该可以看出来,函数执行真正的符号是 “()” , 前面的基本等同于是个标识,所以算是 锱铢必较...

四、执行上下文栈

  执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。

  这其实是一个压栈出栈的过程 -- 执行上下文栈。如下图:

  一段具体的代码事例:

  1. var a = 10, // 1.进入全局上下文环境
  2. fn,
  3. bar = function(x) {
  4. var b = 5;
  5. fn(x + b); // 3.进入 fn 函数上下文环境
  6. };
  7.  
  8. fn = function(y) {
  9. var c = 5;
  10. console.log(y + c);
  11. }
  12.  
  13. bar(10); // 2.进入 bar 函数上下文环境

首先,在执行第 1 行代码前,创建一个全局上下文环境。

然后,执行代码。在 12 行之前,上下文中的变量均被赋值。

接着,到 13 行,调用 bar 函数。跳转到 bar 函数的内部,执行函数体语句之前,会创建一个新的执行上下文环境。

并将这个上下文环境压栈,设置为活动状态。

因为在第 5 行又调用了 fn 函数,进入 fn 函数,在执行函数体语句之前,会创建 fn 函数的执行上下文环境,并压栈,设置为活动状态。

在第 5 行执行完毕,即 fn 函数执行完毕后,此次调用 fn 所产生的上下文环境出栈,并且被销毁(释放内存)。

同样, bar 函数执行完毕后,调用 bar 函数所生成的上下文环境出栈,并且被销毁(释放内存)。

以上,是一段代码执行时全局、函数体的上下文执行环境的变化理论过程。实际情况往往跟复杂。

五、作用域

  一些注意点:

    1.“ javascript 没有块级作用域”。块,即“{ ... }”。例如 if/for 语句。

    2. javascript 除了全局作用域之外,只有函数可以创建的作用域。

  所以一个好的习惯就是在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用“单var”形式。比如:

  1. var i = 10;
  2. if (i > 1) {
  3. // code
  4. }
  5.  
  6. var i;
  7. for (i = 0; i < 10; i++) {
  8. // code
  9. }

  

  作用域,通俗意义上相当于“地盘”,其作用就是隔离变量,防止冲突。正如前面第 三 点中提到的,函数在定义时,其作用域就已经确定了,而不是在函数调用时确定。

  1. var a = 10, b = 20;
  2.  
  3. function fn(x) {
  4. var a = 100, c = 300;
  5. function bar(x) {
  6. var a = 1000, d = 4000;
  7. }
  8. bar(100);
  9. bar(200);
  10. }
  11.  
  12. fn(10);

接下来,逐步分析:

  第一步,在加载程序时,已经确定全局上下文环境,并随着程序的执行对变量进行赋值。

  第二步,程序执行到第 17 行时,调用 fn(10) ,此时生成此次调用 fn 函数时的上下文环境,压栈,并此上下文环境设置为活跃状态。

  第三步,执行到第 13 行时,调用 bar(100),生成此次调用的上下文环境,压栈,并设置为活动状态。

  第四步,执行完第 13 行,bar(100) 调用结束, bar(100) 上下文环境被销毁。接着执行第 14 行,调用 bar(200) ,则有生成 bar(200) 的上下文环境,压栈,设置为活动状态。

  第五步,执行完第 14 行,bar(200) 调用结束,上下文环境被销毁。回到 fn(10) 上下文环境,变回活动状态。

  第六步,执行完第 17 行,fn(10) 调用结束,上下文环境被销毁。回到全局上下文环境,变回活动状态。

  总结:

    作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。(作用域只会产生一次,上下问环境多次产生多次销毁)

六、自由变量以及作用域链

  自由变量:在 A 作用域内使用变量 x , 但是 A 的作用域内却没有 x 变量的声明(即在其他作用域内声明的),对于 A 作用域来说, x 就是自由变量。

  先看下面代码:

  1. var x = 10;
  2. function fn() {
  3. console.log(x);
  4. }
  5.  
  6. function show(f) {
  7. var x = 20;
  8.  
  9. // 这是个匿名函数
  10. (function() {
  11. f();
  12. })()
  13. }
  14.  
  15. show(fn);
  16.  
  17. // 10,而不是20

  

  代码中,对于函数 fn 来说, 其中的 x 就是自由变量。

  然后,对于函数中自由变量的取值,要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”。

  

  所以,在上面的代码中,匿名函数在 show 函数中创建,所以其中的自由变量应该是到 show 函数的作用域中取值;但是,匿名函数中并没有自由变量,只是执行了 f (即 fn) 函数,而 fn 函数是在全局作用域的环境中创建,所以其中的自由变量是到全局作用域中取值。故,最终打印的 x 是 10。

  

  作用域链:上面的例子中 fn 是在全局环境下创建的,那么如果 fn 和 全局作用域间隔着 N + 1 (N >= 0) 个函数呢?这样就形成了作用域链。

       如果一个子函数在父函数(声明这个子函数所在的函数体)中没有找到所需要的自由变量,那么该子函数会去祖父函数中去找 ... 并以此类推。

  1. var a = 10;
  2. function fn() {
  3. var b = 20;
  4.  
  5. function bar() {
  6. console.log(a + b);
  7. }
  8.  
  9. return bar;
  10. }
  11.  
  12. var x = fn(),
  13. b = 200;
  14.  
  15. x();
  16.  
  17. // 30

七、闭包(千呼万唤始出来)

  闭包这个概念不太好理解,但我们可以记住它的运用场合 —— 函数作为返回值,函数作为参数传递。

  注意,这里开始需要上面 执行上下文、作用域 的基础。

  关键点整理如下:

  1.函数作用域在定义时即创建,而非在调用时。

  2.函数执行前会对产生执行上下文环境,此时才会对函数体内的变量声明以及赋值。

  3.一般情况下,函数调用执行完成,执行上下文环境即被销毁。

  4.函数真正执行的语句是 “()”,而不是 “函数名()”。

  既然是一般情况下,那么特殊情况是什么呢? —— 闭包的两个运用场合下,执行上下文环境不会被销毁。 —— 闭包的核心。

  看图说话:

  1. function fn() {
  2. var max = 10;
  3.  
  4. function bar(x) {
  5. if (x > max) {
  6. console.log(x);
  7. }
  8. }
  9.  
  10. return bar;
  11. }
  12.  
  13. var f1 = fn(),
  14. max = 100;
  15. f1(15);

  

  步骤分析:

  1.代码执行前生成全局上下文环境,并在执行时对其中的变量进行赋值。此时全局上下文环境是活动状态。

  2.执行第17行代码时,调用fn(),产生fn()执行上下文环境,压栈,并设置为活动状态。

  3.执行完第17行,fn()调用完成。按理,应销毁 fn 的执行上下文环境,但是因为 fn 的返回值是 bar 函数,bar 函数中 max 变量是自由变量,其需要引用 fn 执行上下文环境中的 max 变量,因此,fn 的执行上下文环境不能被销毁,否则 bar 函数中 max 将找不到值。

  4.执行到第18行时,全局上下文环境将变为活动状态,但是fn()上下文环境依然会在执行上下文栈中。另外,执行完第18行,全局上下文环境中的max被赋值为100。

  5.执行到第20行,执行f1(15),即执行bar(15),创建bar(15)上下文环境,并将其设置为活动状态。

  6.执行完20行就是上下文环境的销毁过程,依次是 bar - fn - 全局 。

  

  至此,原型、闭包核心内容结束。贴个代码以及分析:

  1. function createFunctions(){
  2. var result = new Array();
  3. for (var i=0; i < 10; i++){
  4. result[i] = function(){
  5. return i;
  6. };
  7. }
  8. return result;
  9. }
  10. var funcs = createFunctions();
  11. for (var i=0; i < funcs.length; i++){
  12. console.log(funcs[i]());
  13. }

  分析:

  1. var result = new Array(), i;
  2. result[0] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换!
  3. result[1] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换!
  4. ...
  5. result[9] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换!
  6. i = 10;
  7. funcs = result;
  8. result = null;
  9.  
  10. console.log(i); // funcs[0]()就是执行 return i 语句,就是返回10
  11. console.log(i); // funcs[1]()就是执行 return i 语句,就是返回10
  12. ...
  13. console.log(i); // funcs[9]()就是执行 return i 语句,就是返回10

  解决方法:函数 createFunctions 中 for 循环的 var i = 0 换成 let i = 0 即可。

JS 原型和闭包的更多相关文章

  1. 【学习笔记】深入理解js原型和闭包系列学习笔记——精华

    深入理解js原型和闭包笔记: 1.“一切皆是对象”,对象是属性的集合. 丨 函数也是对象,但是使用typeof时为什么函数返回function而 丨  不是object呢,js为何要对函数做这样的区分 ...

  2. 【学习笔记】深入理解js原型和闭包(18)——补充:上下文环境和作用域的关系

    本系列用了大量的篇幅讲解了上下文环境和作用域,有些人反映这两个是一回儿事.本文就用一个小例子来说明一下,作用域和上下文环境绝对不是一回事儿. 再说明之前,咱们先用简单的语言来概括一下这两个的区别. 0 ...

  3. 【学习笔记】深入理解js原型和闭包(17)——补this

    本文对<深入理解js原型和闭包(10)——this>一篇进行补充,原文链接:https://www.cnblogs.com/lauzhishuai/p/10078307.html 原文中, ...

  4. 【学习笔记】深入理解js原型和闭包(16)——完结

    之前一共用15篇文章,把javascript的原型和闭包讲解了一下. 首先,javascript本来就“不容易学”.不是说它有多难,而是学习它的人,往往都是在学会了其他语言之后,又学javascrip ...

  5. 【学习笔记】深入理解js原型和闭包(15)——闭包

    前面提到的上下文环境和作用域的知识,除了了解这些知识之外,还是理解闭包的基础. 至于“闭包”这个词的概念的文字描述,确实不好解释,我看过很多遍,但是现在还是记不住. 但是你只需要知道应用的两种情况即可 ...

  6. 【学习笔记】深入理解js原型和闭包(14)——从【自由变量】到【作用域链】

    先解释一下什么是“自由变量”. 在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量.如下图 如上程序中,在调用fn()函数时,函数体中第6 ...

  7. 【学习笔记】深入理解js原型和闭包(13)——【作用域】和【上下文环境】

    上文简单介绍了作用域,本文把作用域和上下文环境结合起来说一下,会理解的更深一些. 如上图,我们在上文中已经介绍了,除了全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了.而不 ...

  8. 【学习笔记】深入理解js原型和闭包(12)——简介【作用域】

    提到作用域,有一句话大家(有js开发经验者)可能比较熟悉:“javascript没有块级作用域”.所谓“块”,就是大括号“{}”中间的语句.例如if语句: 再比如for语句: 所以,我们在编写代码的时 ...

  9. 【学习笔记】深入理解js原型和闭包(11)——执行上下文栈

    继续上文的内容. 执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境.当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境.处于活动状态的执行 ...

  10. 【学习笔记】深入理解js原型和闭包(10)——this

    接着上一节讲的话,应该轮到“执行上下文栈”了,但是这里不得不插入一节,把this说一下.因为this很重要,js的面试题如果不出几个与this有关的,那出题者都不合格. 其实,this的取值,分四种情 ...

随机推荐

  1. Spring Security(二十六):8. Spring Security Community

    8.1 Issue Tracking Spring Security uses JIRA to manage bug reports and enhancement requests. If you ...

  2. Python+Pycharm—学习—pip

    1.pip是干什么的? 2.pip怎么安装? 3.pip怎么用?

  3. intoj

    192.168.0.6:1024 emm....我太健忘了...

  4. vue是一个渐进式的框架,如何理解“渐进式”

    每个框架都不可避免会有自己的一些特点,从而会对使用者有一定的要求,这些要求就是主张,主张有强有弱,它的强势程度会影响在业务开发中的使用方式.使用vue,你可以在原有大系统的上面,把一两个组件改用它实现 ...

  5. centos7 开放3306端口并可以远程访问

    开启远程访问: GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'root' WITH GRANT OPTION; 允许任何ip以roo ...

  6. centos 7 安装 MySQL 5.6

    由于Centos7 默认数据库是mariabd,所以通过rpm安装MySQL需要卸载原有的Mariabd,再下载所有的依赖包比较麻烦且容易出错.通过yum的方式安装.yum其实是rpm的一个升级将所有 ...

  7. Spark访问与HBase关联的Hive表

    知识点1:创建关联Hbase的Hive表 知识点2:Spark访问Hive 知识点3:Spark访问与Hbase关联的Hive表 知识点1:创建关联Hbase的Hive表 两种方式创建,内部表和外部表 ...

  8. ASP.NET Core 发布之后通过命令控制监听地址和环境变量

    添加Command支持 新建一个ASP.NET Core 项目,打开Program.cs 添加下面的代码: public class Program { public static void Main ...

  9. jquery tooltip

    这是个加了点淡入淡出效果的顶部tooltip控件,会自动消失 用法: <head> <title></title> <link href="base ...

  10. 为什么大公司一定要使用DevOps?

    0 DevOps的意图 究竟什么是DevOps? 要想回答这个问题,首先要明确DevOps这个过程参与的人员是谁?即开发团队和IT运维团队!那么,DevOps的意图是什么呢?即在两个团队之间,建立良好 ...