深入理解JavasCript原型和闭包

1.对象是什么

  • 对象就是若干属性的集合。

  • 在JS中一切引用类型都是对象:数组是对象,函数是对象,对象还是对象。对象里面的一切都是属性,只有属性,没有方法。那么这样方法如何表示呢?方法也是一种属性。因为它的属性表示为键值对的形式。如:

    var obj = {
      a: 10,
      b: function() {},
      c: {
        c1: '100',
        c2: function() {}
      }
    }
    
  • 上面说到函数、数组都是对象,那么它们也可以随意添加属性。如:

    function fn() {}
    fn.a = 10
    fn.b = function() {
      return 100
    }
    这个特性有什么作用呢?
    jQeury中'$'或者'jQuery',这个变量其实是一个函数。
    typeof $  // function
    而通过$可以调用很多方法,如:$.trim('abc ') //'abc'
    很明显这是在$函数上加了一个trim属性,而这个属性是一个函数。
    
  • 如何判断一个变量是否是对象:xxx instanceof Object

2.函数和对象的关系

  • 第一节说到,函数是一种对象,因为通过instanceof函数可以判断。但是函数与对象之间,却不仅仅是一种包含和被包含的关系,函数和对象之间的关系比较复杂,甚至有一点鸡生蛋蛋生鸡的逻辑。如:

    function fn() {
      this.name = 'bill',
      this.age = '99'
    }
    var fn1 = new fn()
    // fn1:{name: "bill", age: "99"}
    这个例子说明了对象可以通过函数来创建。
    
  • 其实所有对象都是通过函数来创建的,有人可能有疑问:

    var obj = {}
    var arr = []
    同过这种方式也能创建对象啊!
    但这种方式只是‘快捷方式’而已,在编程语言中一般称其为‘语法糖’。以上代码的本质是:
    var obj = new Object()
    var arr = new Array()
    typeof Object // function
    typeof Array  // function
    综上,可以说对象都是通过函数来创建的。
    

3.prototype(原型)

  • 每个函数都默认有个prototype属性,这个属性的值是一个对象,而这个对象默认只有一个属性constructor,指向这个函数本身。如图:(左侧是一个函数,而右侧是它的原型)


  • 既然prototype是个对象,那么就可以为它们添加属性:

    function fn () {}
    fn.prototype.name = 'bill'
    fn.prototype.age = function () {
      return 99
    }
    
  • 每个对象都有一个隐藏属性——"__proto__",这个属性引用了创建这个对象的函数的prototype。也就是说,每个对象都可以共同使用它们的父函数的原型对象中的属性。

    第一节例子中所说的jQery的其实就是根据这个原理实现的:
    $.prototype.trim = function() {
      ....
    }
    
  • 函数的prototype也是一个对象,那么这个对象也有一个"__proto_"属性,它指向Object.prototype。但是Object.prototype是一个特例——它的__proto_指向的是null

  • 上面所过函数也是一个对象,那么函数的"__proto__"属性指向什么?

    function fn(){} 等价于=》
    var fn = new Function()
    所以说函数的\_\_proto__就指向Function的prototype。
    而Funtion也是一个函数,既然是函数,那么它一定是被Function创建,它的\_\_proto__就指向它自身的prototype。
    

4.instanceof

  • instanceof用于判断一个对象的类型,那么它的判断规则是什么?
    • Instanceof运算符的第一个变量是一个对象,暂时称为A;第二个变量一般是一个函数,暂时称为B。

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

      通过以上规则可以解释很多怪异现象
      Object instanceof Function //true
      //Object是一个函数,所以Object.__proto__指向Function.prototype
      
      Function instanceof Object //true
      //Funtion是一个函数,所以Function.__proto__指向Function.prototype,而Function.prototype.__proto__指向Object.prototype
      

5.继承/原型链

  • JS中的继承是通过原型链实现的,先看下面的例子:

    function Foo() {}
    var f1 = new Foo()
    f1.a = 10
    Foo.prototype.a = 100
    Foo.prototype.b = 200
    f1.a //10
    f1.b //200
    以上代码中,f1是Foo函数new出来的对象,f1.a是f1对象的基本属性,f1.b是怎么来的呢?——从Foo.prototype得来,因为f1.__proto__指向的是Foo.prototype
    
  • 访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着proto这条链向上找,这就是原型链。

  • 那么我们在实际应用中如何区分一个属性到底是基本的还是从原型中找到的呢?大家可能都知道答案了——hasOwnProperty,特别是在for…in…循环中,一定要注意。

    f1.hasOwnProperty(b) // false
    f1本身没有这个方法,它是从Object.prototype中来的。如图
    
  • 由于所有的对象的原型链都会找到Object.prototype,因此所有的对象都会有Object.prototype的方法。这就是所谓的“继承”。你可以利用原型链来实现自己的继承。

  • 如果继承的方法不合适,可以做出添加/修改。

    var obj = {a: 1, b: 2}
    obj.toString()  //[object Object]
    var arr = [1, 2, 3]
    arr.toString() // 1,2,3
    Object和Array的toString()方法不一样。肯定是Array.prototype.toString()方法做了修改。
    

6.执行上下文

  • 先看一段代码:

    console.log(a)  // referenceError
    
    console.log(b) // undifined
    var b
    
    console.log(c) // undifined
    var c = 1
    
    第一句报错,a未定义,很正常。第二句、第三句输出都是undefined,说明浏览器在执行console.log(b)时,已经知道了b是undefined,但却不知道c是1。
    
  • 在一段js代码拿过来真正一句一句运行之前,浏览器已经做了一些“准备工作”,其中就包括对变量的声明,而不是赋值。变量赋值是在赋值语句执行的时候进行的。可用下图模拟:


  • 第二种情况:第一种情况只是对变量进行声明(并没有赋值),而此种情况直接给this赋值。这也是“准备工作”情况要做的事情之一。


  • 第三种情况:需要注意代码注释中的两个名词——“函数表达式”和“函数声明”。虽然两者都很常用,但是这两者在“准备工作”时,却是两种待遇。


  • 在“准备工作”中,对待函数表达式就像对待“ var a = 10 ”这样的变量一样,只是声明。而对待函数声明时,却把函数整个赋值了。

  • 我们总结一下,在“准备工作”中完成了哪些工作:

    • 变量、函数表达式——变量声明,默认赋值为undefined;
    • this——赋值;
    • 函数声明——赋值;
    • 这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。
  • javascript在执行一个"代码段"之前,都会进行这些“准备工作”来生成执行上下文。这个“代码段”其实分三种情况——全局代码,函数体,eval代码。eval不常用,也不推荐大家用。

  • 如果在函数中,除了以上数据之外,还会有其他数据。先看以下代码:


  • 以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。从这里可以看出,函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。

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


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

7.this

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

  • this的取值,分四种情况。

  • 情况1:构造函数:所谓构造函数就是用来new对象的函数。

    • 其实严格来说,所有的函数都可以new一个对象,但是有些函数的定义是为了new一个对象,而有些函数则不是。
    • 如果函数作为构造函数用,那么其中的this就代表它即将new出来的对象。
    • 不仅仅是构造函数的prototype,即便是在整个原型链中,this代表的也都是当前对象的值。
  • 情况2:函数作为对象的一个属性

    • 如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数中的this指向该对象。
  • 情况3:函数用call或者apply调用

    • 当一个函数被call和apply调用时,this的值就取传入的对象的值。
  • 情况4:全局 & 调用普通函数

    • 在全局环境下,this永远是window.
    • 普通函数在调用时,其中的this也都是window。

8.执行上下文栈

  • 执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。这是一个压栈出栈的过程。如下图(蓝色表示活动状态):


9.作用域

  • “javascript没有块级作用域”。所谓“块”,就是大括号“{}”中间的语句。所以,我们在编写代码的时候,不要在“块”里面声明变量,要在代码的一开始就声明好了。以避免发生歧义。
  • javascript除了全局作用域之外,只有函数可以创建的作用域。
  • 所以,我们在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用“单var”形式。
  • 如上图,全局代码和fn、bar两个函数都会形成一个作用域。而且,作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。例如,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。
  • 作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
  • 除了全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时确定。
  • 作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。
  • 如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。
  • 什么是“自由变量”:
    • 在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。
    • 那么到哪里去取自由变量呢? 要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”
    • 如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。这个一步一步“跨”的路线,我们称之为——作用域链。

10.闭包

  • 在一个作用域中引用另一个作用域中的变量就形成了闭包。
  • 闭包应用的两种情况:
    • 第一,函数作为返回值:


    • 如上代码,bar函数作为返回值,赋值给f1变量。执行f1(15)时,用到了fn作用域下的max变量的值。
    • 第二,函数作为参数被传递:


    • 如上代码中,fn函数作为一个参数被传递进入另一个函数,赋值给f参数。执行f(15)时,max变量的取值是10,而不是100。
  • 有些情况下,函数调用完成之后,其执行上下文环境不会接着被销毁。这就是需要理解闭包的核心内容。
  • 如上图当bar()被作为函数返回时,fn函数执行完毕,正常情况下fn的执行上下文环境应该被销毁,而此时bar函数中有一个自由变量max引用自fn的执行上下文,所以fn的执行上下文就不会被销毁,否则就找不到max的值了。

11.上下文环境和作用域的关系

  • 上下文环境:
    • 储存着作用域中所有变量的对象。
    • 对于函数来说,上下文环境是在调用时创建的,这个很好理解。拿参数做例子,你不调用函数,我哪儿知道你要给我传什么参数?
  • 作用域:
    • 首先,它很抽象。第二,记住一句话:除了全局作用域,只有函数才能创建作用域。创建一个函数就创建了一个作用域,无论你调用不调用,函数只要创建了,它就有独立的作用域,就有自己的一个“地盘”。
  • 关系:
    • 一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容