学习总结:《你不知道的JavaScript》第二部分 This和对象原型

122 阅读10分钟

一、关于this

  1. 核心:

    1. this是在运行时进行绑定的,而非编写时绑定,它的上下文取决于函数调用时的各种条件。
    2. this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
    3. this既不指向函数自身也不指向函数的词法作用域。
    4. this实际上是在函数“被调用”时发生的绑定,它指向什么完全取决于“函数在哪里被调用”。
  2. 以上核心点均强调“调用”,函数调用会生成调用栈,分析函数在调用栈中的真正调用位置,决定了this的绑定。(可以借助浏览器的开发者工具通过打断点的方式来查看当前函数的调用栈)

  3. 绑定规则:

    1. 默认绑定:

      1. 严格模式下,不能将全局对象用于默认绑定,此时this指向undefined
      2. 非严格模式下,this指向window
    2. 隐式绑定:

      1. 示例: image.png

      2. 无论foo直接在obj中定义还是在全局中定义后再添加为引用属性,这个函数严格来说均不属于obj。

      3. foo本与obj无关,“obj.foo()”这个写法使得foo置于obj的上下文中,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。此时this.a等价于obj.a。

      4. 当出现对象属性引用链时,只有最后一层生效。例如 obj1 和 obj2 中同时拥有a,obj1.obj2.a,此时的a为 obj2 中的a。

      5. 隐式丢失:被隐式绑定的函数会丢失绑定对象,它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。

    3. 显式绑定:

      1. 方法:通过call和apply来改变this的指向。

      2. 语法:参数一为this绑定的对象,区别体现在为枚举还是数组形式。(小tips:如果参数一为字符串/数字/布尔则会被转换为new String()/new Number()/new Boolean()形式)

      3. es5方法:通过bind,bind返回一个新函数,会将指定的参数设置为this的上下文并调用原始函数。

        1. 硬绑定:通过硬绑定可以解决显式绑定出现的丢失绑定问题。

          1. 示例:image.png
          2. 典型应用场景:创建一个包裹函数,负责接收参数并返回值。
        2. API上下文:许多函数、方法都提供了可选参数context,作用和bind相同,确保回调函数使用指定的this。(例如:[].forEach(fn,obj)此时的obj为context)

    4. new绑定:传统面向类的语言中,“构造函数”是类中的特殊方法,使用new初始化类会调用类中的构造函数。而在JavaScript中构造函数只是new时被调用的函数,并不属于某一个类,也不会实例化一个类。

      1. 绑定过程:

        1. 创建一个全新对象。                             

        2. 设置原型,将对象原型设置成函数的prototype对象。   

        3. 新对象绑定到函数调用的this。                    

        4. 判断函数返回值类型。(如果是值类型,返回创建的对象,如果是引用类型,返回引用类型的对象)                           

      2. 对应代码示例:

        1. var obj = new Xxxx();

        2. obj.proto = Xxxx.prototype;

        3. var result = Xxxx.call(obj);

        4. return typeof result  === 'object' ? result : obj;

  4. 绑定优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。

  5. 软绑定softBind():解决硬绑定降低函数灵活性问题,使用硬绑定后无法通过隐式绑定或者显式绑定来修改this。

  6. 简单的this判断思路:

    1. image.png
    2. 一些例外情况:当将null或者undefined作为绑定对象传入call/apply/bind时,这些值在调用时会被忽略,仍然使用默认绑定。
  7. 箭头函数的this在声明时就确定了,会继承外层函数调用的this绑定。

  8. 有些调用可能无意中使用了默认绑定规则,可以使用DMZ对象(demilitarized zone,一个空的非委托对象)来更安全的忽略this绑定,var dmzObj = Object.create(null)。相对于{},使用Object.create(null)更好,因为它不会创建Object.prototype这个委托,比{}“更空”。

  9. polyfill:用于旧浏览器的兼容。

二、对象

  1. typeof null为对象的原理:不同对象在底层都表示为二进制,JavaScript中二进制前三位均为0则会判断为object类型,null的二进制表示全为0,自然也为object类型。

  2. 在Js引擎内部,对象内部属性值的存储方式多种多样,一般不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,类似指针,指向值的真正存储位置。

  3. 在对象中,属性名永远是字符串,即使使用其他值,也会被转化为字符串。

  4. 对象的访问方式:

    1. 属性访问:(obj.abc)属性名须符合标识符命名规范。

    2. 键访问:(obj.['abc'])接受UTF-8/Unicode字符串。

  5. 深拷贝方案:JSON.parse( JSON.stringify( Obj ) ),先转化成JSON字符串,再根据其解析出对象。

  6. 浅拷贝方案:Object.assign(..),参数一为目标对象,剩余参数可以是一个或多个源对象。

  7. 可计算属性名:ES6新增,可在文字形式中使用[]来包裹表达式当做属性名。

    1. 示例:image.png
  8. 数组可以通过 “ . ” / “ [ ] ” 语法来添加命名属性,但数组长度不会增加。

    1. 示例:image.png

    2. 注:若添加命名看起来像数字,则会被默认转化为下标。

      1. 示例:image.png
  9. 属性(数据)描述符:可通过 Object.getOwnPropertyDescriptor(obj,prop) 和 Object.defineProperty(obj,prop,description) 来取得给定属性的描述符和定义描述符。

    1. 数据描述符:

      1. value:undefined;       值内容。

      2. writable:true;     是否可写,即修改value。

      3. configurable:true;     属性是否可配置,即对writable和enumerable的设置权限,且为false还会禁止删除当前属性。(configurable不可逆,一旦设置false,不能改回true)

      4. enumerable:true;       是否可枚举,即能否通过for..in返回属性。

    2. 应用:

      1. Object.seal()密封:创建一个“密封”对象(不允许添加、配置、删除任何现有属性),实际是在当前对象调用object.preventExtensions()并设置所有configurable:false。(Object.preventExtensions()禁止拓展:禁止对象添加新属性并保留已有属性)
      2. Object.freeze()冻结:创建“冻结”对象(无法修改对象的值),在现有对象上调用 Object.seal() 并设置所有writable:false。
      3. writable:false + configurable:false 即可创建真正的常量属性。
  10. [[Get]]操作:获取属性值

    1. 示例:image.png
    2. 当我们在进行属性访问时,语言规范内部实际上是进行了[[Get]]操作(等价[Get]这一函数调用),它会首先在当前对象中先查找a,若查找不到,则会遍历可能存在的[[prototype]]链。若依然查找不到,则[[Get]]操作返回undefined。
  11. [[Put]]操作:更新属性值

  12. Getter和Setter函数可以局限的改写对象默认的[[Get]]和[[Put]]操作。

    1. 局限性:只能应用在单个属性上,无法应用在整个对象上。(所以在vue框架的响应式实现原理中,新组件创建时,Observer需要对data中每个对象进行逐层递归遍历添加上Getter和Setter,当层级深时会带来较多的时间消耗)
  13. 访问描述符:可通过 Object.getOwnPropertyDescriptor(obj,prop) 和 Object.defineProperty(obj,prop,description) 来取得给定属性的描述符和定义描述符。

    1. 访问描述符:

      1. get:       读取属性所调用的函数。

      2. set:       写入属性所调用的函数。

      3. configurable:true;

      4. enumerable:true;       当为false时,依然可以通过obj.xx语法访问到,但是无法通过for..in遍历出来,相当于不可以出现在对象属性的遍历中。

  14. 存在性

    1. in 操作符:

      1. 语法:"a" in xxx
      2. 含义:会检查属性a是否在当前对象及其[[prototype]]中,返回布尔值。
      3. 注意:当xxx为数组时,检查的是'a'这一属性名(key)是否存在,即下标a是否存在。
    2. hasOwnProperty(..):

      1. 语法:xxx.hasOwnProperty('a')

      2. 含义:只检查属性a是否存在对象xxx中,不找原型链。

    3. propertyIsEnumerable():

      1. 语法:xxx.propertyIsEnumerable('a')

      2. 含义:'a'是否直接存在于对象xxx中,不找原型链。

  15. 一些遍历方式:

    1. Object.keys():返回一个数组,包含所有可枚举属性。

    2. Object.getOwnPropertyNames():返回一个数组,包含所有属性(可枚举&不可枚举)。

    3. for..in:遍历对象的可枚举属性,包括原型链。

    4. some:一直遍历到true,一真即返回真,会提前终止遍历。

    5. every:一直遍历到false,一假即返回假,会提前终止遍历。

    6. forEach:遍历数组所有元素,没有返回。

    7. for..of:遍历的是值而不是数组下标,前提是具有@@iterator迭代器属性的数据类型,数组具备,有些具备迭代器属性的对象月可以使用。

      1. 内部工作原理:image.png

三、混合对象“类”

  1. 类是一种设计模式。
  2. 混入:其他语言中“类”表现出来的都是复制行为,js中一个对象并不会复制到其它对象,只会“关联”,js用“混入”来模拟类的复制行为。
  3. JavaScript 和面向类的语言不同,它并没有类来作为对象的抽象模式。JavaScript 中只有对象。

四、原型

  1. 原型链链顶可以认为是 Object.prototype 或者 Object.prototype.proto=null。

  2. 属性设置和屏蔽:

    1. 示意图:image.png
  3. 在面向类的语言中,类可以被复制(或者说实例化)多次,但在 JavaScript 中,并没有类似的复制机制,所以不能创建一个类的多个实例,只能创建多个对象,它们 [[Prototype]] 关联的是同一个对象。

    1. 示意图:image.png
  4. constructor指向当前原型对象的构造函数。

  5. isPrototypeOf(..)属性:

    1. 语法:Foo.prototype.isPrototypeOf( a )
    2. 含义:在 a 的整条 Prototype 链中是否出现过 Foo.prototype。
  6. getPrototypeOf(..)属性:

    1. 语法:Object.getPrototypeOf( a )
    2. 含义:获取对象 a 的 Prototype 链。
  7. setPrototypeOf(..)属性:

    1. 语法:Object.setPrototypeOf( this, o )
    2. 含义:设置对象 的 Prototype 链。
  8.  _proto_的实现原理:

    1. 示例:image.png
  9. Object.create的实现原理:

    1. 示例:image.png
  10. 委托设计模式:

    1. 直接委托:image.png

      虽然通过prototype也能正确工作,如果目的只是为了在myObject上找不到cool时,可以使用备用的anotherObject,则可以换一种写法。

    2. 内部委托:image.png

五、行为委托

  1. ES6的简洁语法的重要缺点:

    1. 示例:image.png

    2. 由于函数对象本身没有名称标识符,所以bar()的缩写形式(function()..)实际上会变成一个匿名函数表达式并赋值给 bar 属性。相比之下,具名函数表达式(function baz()..)会额外给 .baz 属性附加一个词法名称标识符 baz。

    3. 这会牵扯到匿名函数表达式的三个缺陷:

      1. 调用栈难追踪
      2. 自我引用(递归、事件绑定等)
      3. 代码语义化程度降低
    4. 此时,简洁方法比较特殊,它会给对应的函数对象内部设置一个内部的name属性,这样可以确保不出现 i、iii 缺陷,然而缺陷 ii 依赖的是自我引用的词法标识符,该方式不具备其。

  2. 内省:检查实例类型。

  3. 行为委托设计模式:行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript 的 Prototype 机制本质上就是行为委托机制。

  4. 不建议使用”类“:

    1. 在 JavaScript 不存在类。即使是ES6的”类“,仍是prototype机制的语法糖。

    2. 在 JavaScript 这样的Prototype语言中实现类是很别扭的,依赖于prototype引用实现的 JavaScript 类内部存在繁琐杂乱的.prototype引用。

    3. 传统面向类的语言中,子类和父类、子类和实例之间其实均为复制关系。而在JS的prototype中并没有复制,他们之间只有委托关联。

    4. super的内部缺陷:

      1. 示例:image.png
      2. 咱们期望 super() 会自动识别出 E 委托了 D,所以 E.foo() 中的 super() 应该调用 D.foo(),然而并不是。super并不像this是动态绑定的,而是静态绑定。