美文网首页让前端飞你不知道的JavaScript前端开发
《你不知道的js(上卷)》笔记2(this和对象原型)

《你不知道的js(上卷)》笔记2(this和对象原型)

作者: 陨石坠灭 | 来源:发表于2019-06-01 21:27 被阅读1次

    学了多种语言,发现javascriptthis是最难以捉摸的。this不就是指向当前对象的指针吗?可是结合上下文来看,却又往往不知道this到底指的是谁了,所以Javascript最主要的两个知识点,除了闭包,就是this了。

    1. 关于this

    this关键字是javascript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在 所有函数的作用域中。

    this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计
    得更加简洁并且易于复用。

    function identify() {
      return this.name.toUpperCase();
    }
    
    var me = {
      name: "Kyle"
    };
    
    identify.call( me ); // KYLE
    

    this并不像我们所想的那样指向函数本身。

    function foo(num) {
      this.count++;
    }
    
    foo.count = 0;
    var i;
    for (i=0; i<10; i++) { 
          if (i > 5) {
            foo( i ); 
          }
    }
    console.log( foo.count ); // 0 
    

    函数内部代码this.count中的this并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相同,困惑随之产生。

    函数内部代码this.count最终值为NaN,同时也是全局变量。

    可以使用函数名称标识符来代替this来引用函数对象。这样,更像是静态变量。

    function foo(num) {
      foo.count++;
    }
    
    foo.count = 0;
    var i;
    for (i=0; i<10; i++) { 
          if (i > 5) {
            foo( i ); 
          }
    }
    console.log( foo.count ); // 4
    

    另外一种方式是强制this指向foo函数对象。

    function foo(num) {
      this.count++;
    }
    
    foo.count = 0;
    var i;
    for (i=0; i<10; i++) { 
          if (i > 5) {
            foo.call(foo, i ); 
          }
    }
    console.log( foo.count ); // 4
    

    this到底是什么

    this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

    调用位置

    函数被调用的位置。每个函数的 this 是在调用 时被绑定的,完全取决于函数的调用位置,因为它决定了this的绑定。

    function baz() {
    // 当前调用栈是:baz
    // 因此,当前调用位置是全局作用域
       console.log( "baz" );
        bar(); // <-- bar 的调用位置 
    }
    
    function bar() {
        // 当前调用栈是 baz -> bar
        // 因此,当前调用位置在 baz 中
        console.log( "bar" );
        foo(); // <-- foo 的调用位置 
    }
    
    function foo() {
            // 当前调用栈是 baz -> bar -> foo 
            // 因此,当前调用位置在 bar 中
             console.log( "foo" );
    }
    baz(); // <-- baz 的调用位置
    

    1.1 绑定规则

    默认绑定

    声明在全局作用域中的变量就是全局对象的一个同名属性。

    function foo() { 
      console.log( this.a );
    }
    
    var a = 2; 
    foo(); // 2
    

    在本 例中,函数调用时应用了this的默认绑定,因此this指向全局对象。

    foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。

    如果使用严格模式,那么全局对象将无法使用默认绑定,因此this会绑定到 undefined。

    function foo() {
       "use strict";
        console.log( this.a );
    }
    var a = 2;
    foo(); // TypeError: this is undefined
    

    隐式绑定

    如果调用位置是有上下文对象,或者被某个对象拥有或者包含,那么就可能隐式绑定。

    function foo() { 
      console.log( this.a );
    }
    var obj = { 
      a: 2,
      foo: foo
    };
    
    var obj1 = { 
      a: 42,
      obj: obj
    };
    
    obj.foo(); // 2
    obj1.obj.foo(); // 2
    

    当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调 用foo()this被绑定到obj,因此this.aobj.a是一样的。

    对象属性引用链中只有最顶层或者说最后一层会影响调用位置。

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

    function foo() { 
      console.log( this.a );
    }
    var obj = { 
      a: 2,
      foo: foo 
    };
    var bar = obj.foo; // 函数别名!
    var a = "oops, global"; // a 是全局对象的属性 
    bar(); // "oops, global"
    
    function doFoo(fn) {
        // fn 其实引用的是 foo 
      fn(); // <-- 调用位置!
    }
    
    doFoo( obj.foo ); // "oops, global"
    

    barobj.foo的一个引用,bar()其实是一个不带任何修饰的函数调用。

    参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果一样。

    显式绑定

    可以使用函数的call(..)apply(..)方法实现显式绑定。

    function foo() { 
      console.log( this.a );
    }
    var obj = {
       a:2
    };
    foo.call( obj ); // 2
    

    如下例子,无论bar绑定到哪个对象上,foo始终绑定在obj上,称之为硬绑定。

    function foo() { 
      console.log( this.a );
    }
    var obj = { 
      a:2
    };
    var bar = function() { 
      foo.call( obj );
    };
    
    bar.call( window ); // 2
    

    在 ES5 中提供了内置的方法Function.prototype.bind就是硬绑定。

    如果你把null或者undefined作为this的绑定对象传入callapply或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。

    new绑定

    JavaScriptnew的机制实 际上和面向类的语言完全不同。

    JavaScript中,构造函数只是一些 使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。

    使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

    1. 创建(或者说构造)一个全新的对象。

    2. 这个新对象会被执行[[原型]]连接。

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

    4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

    function foo(p1,p2) { 
      this.val = p1 + p2;
    }
    // 之所以使用 null 是因为在本例中我们并不关心硬绑定的 this 是什么 
    // 反正使用 new 时 this 会被修改
    var bar = foo.bind( null, "p1" );
    var baz = new bar( "p2" ); 
    baz.val; // p1p2
    

    绑定规则优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

    箭头函数无法使用以上四种绑定规则。

    function foo() {
    // 返回一个箭头函数 
      return (a) => {
        //this 继承自 foo()
        console.log( this.a ); 
      };
    }
    
    var obj1 = { 
      a:2
    };
    
    var obj2 = { 
      a:3
    };
    
    var bar = foo.call( obj1 );
    bar.call( obj2 ); // 2
    

    2. 对象

    对象的两种形式定义:声明(文字)形式和构造形式。

    var myObj = { 
      key: value
      // ... 
    };
    
    var myObj = new Object(); 
    myObj.key = value;
    
    

    六种主要类型: string,number,boolean,null,undefined,object

    object外的5种类型为简单基本类型,本身并不是对象,但是typeof null会返回字符串 "object"。

    内置对象:String,Number,Boolean,Object,Function,Array,Date,RegExp,Error

    var strPrimitive = "I am a string"; 
    typeof strPrimitive; // "string" 
    strPrimitive instanceof String; // false
    
    var strObject = new String( "I am a string" ); 
    typeof strObject; // "object"
    strObject instanceof String; // true
    // 检查 sub-type 对象
    Object.prototype.toString.call( strObject ); // [object String]
    

    在必要时语言会自动把字符串字面量转换成一个String对象,可以访问属性和方法。

    对于ObjectArrayFunctionRegExp来说,无论使用文字形式还是构 造形式,它们都是对象,不是字面量。

    属性

    属性名永远是字符串,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串。

    ES6 增加了可计算属性名,最常用的场景可能是 ES6 的符号(Symbol)。

    var prefix = "foo";
    var myObject = {
      [prefix + "bar"]:"hello", 
      [prefix + "baz"]: "world"
    };
         
    myObject["foobar"]; // hello
    myObject["foobaz"]; // world
    

    如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成 一个数值下标

    var myArray = [ "foo", 42, "bar" ]; 
    myArray["3"] = "baz"; 
    myArray.length; // 4
    myArray[3]; // "baz"
    

    复制对象

    对于JSON安全的对象来说,有一种巧妙的复制方法:

    var newObj = JSON.parse( JSON.stringify( someObj ) );
    

    ES6 定义了Object.assign(..)方法来实现浅复制。

    属性描述符

    三个特性:writable(可写)、 enumerable(可枚举)和 configurable(可配置)。

    var myObject = { 
      a:2
    };
    
    Object.getOwnPropertyDescriptor( myObject, "a" );
    // {
    // value: 2,
    // writable: true,
    // enumerable: true,
    // configurable: true 
    // }
    
    

    在创建普通属性时属性描述符会使用默认值,我们也可以使用 Object.defineProperty(..)来添加一个新属性或者修改一个已有属性(如果它是configurable)并对特性进行设置。

    var myObject = {};
         Object.defineProperty( myObject, "a", {
             value: 2,
             writable: true, 
             configurable: true, 
             enumerable: true
         } );
         myObject.a; // 2
    

    writable决定是否可以修改属性的值,如果在严格模式下,这 种方法会出错(TypeError)。

    configurable修改成 false 是单向操作,无法撤销!不管是不是处于严格模式,尝 试修改一个不可配置的属性描述符都会出错(TypeError)。

    属性是不可配置时使用 delete也会失败。

    如果把enumerable设置成false,这个属性就不会出现在枚举中(比如for..in循环),虽然仍 然可以正常访问它。

    不变性

    常量: 结合writable:falseconfigurable:false就可以创建一个真正的常量属性(不可修改、 重定义或者删除)

    var myObject = {};
         Object.defineProperty( myObject, "FAVORITE_NUMBER", {
             value: 42,
              writable: false,
              configurable: false 
          });
    

    禁止扩展: 如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(..)

    密封: Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..) 并把所有现有属性标记为configurable:false

    冻结: Object.freeze(..)会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..)并把所有“数据访问”属性标记为writable:false,这样就无法修改它们 的值。

    get和set

    var myObject = {
    // 给 a 定义一个 getter 
      _a:2,
      get a() {
        return this.a; 
      },
    // 给 a 定义一个 setter 
      set a(_a){
         this._a = _a;
      }
    };
    
    Object.defineProperty( 
      myObject, // 目标对象 
       "b", // 属性名
      {
      // 描述符
      // 给 b 设置一个 getter
      get: function(){ 
          return this.a * 2 
        },
          // 确保 b 会出现在对象的属性列表中
         enumerable: true
        }
    );
    
    myObject.a; // 2
    myObject.b; // 4
    

    在不访问属性值的情况下判断对象中是否存在这个属性:

    var myObject = { 
      a:2
    };
     ("a" in myObject); // true
    ("b" in myObject); // false
    myObject.hasOwnProperty( "a" ); // true
    myObject.hasOwnProperty( "b" ); // false
    

    in操作符会检查属性是否在对象及其 [[Prototype]] 原型链中,相比之下,hasOwnProperty(..)只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。

    有的对象可能没有连接到Object.prototype,可以使用Object.prototype.hasOwnProperty. call(myObject,"a")进行判断。

    propertyIsEnumerable(..)会检查给定的属性名是否直接存在于对象中(而不是在原型链 上)并且满足enumerable:true

    Object.keys(..)会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..)会返回一个数组,包含所有属性,无论它们是否可枚举。

    数组有内置的@@iterator,因此for..of可以直接应用在数组上。

    var myArray = [ 1, 2, 3 ];
    var it = myArray[Symbol.iterator]();
    it.next(); // { value:1, done:false } 
    it.next(); // { value:2, done:false } 
    it.next(); // { value:3, done:false } 
    it.next(); // { done:true }
    

    手动定义@@iterator:

    var myObject = { a: 2,
    b: 3 };
    Object.defineProperty( myObject, Symbol.iterator, { 
      enumerable: false,
      writable: false,
      configurable: true,
      value: function() { 
          var o = this;
          var idx = 0;
          var ks = Object.keys( o ); 
           return {
              next: function() { 
                    return {
                             value: o[ks[idx++]],
                             done: (idx > ks.length)
                         };
            } };
    } } );
    

    3. 原型

    JavaScript中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值。

    对于默认的 [[Get]] 操作来说,第一步是检查对象本身是 否有这个属性,如果有的话就使用它。但是如果不存在与对象本身,就需要会继续访问对象的 [[Prototype]] 链。

    var anotherObject = { 
      a:2
    };
    // 创建一个关联到 anotherObject 的对象
    var myObject = Object.create( anotherObject ); 
    myObject.a; // 2
    

    任何可以通过原型链访问到并且是enumerable的属性都会被枚举。

    使用in操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举)。

    所有普通的 [[Prototype]] 链最终都会指向内置的Object.prototype,它包含 JavaScript中许多通用的功能,比如.toString()

    原型链上层时myObject.foo = "bar"会出现的三种情况:

    • 如果[[Prototype]]链上层存在名为foo的普通数据访问属性并且不是只读,就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性。

    • 如果[[Prototype]]链上层存在名为foo的普通数据访问属性并且只读,则无法修改已有属性或者在 myObject 上创建屏蔽属性。

    • 如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会 调用这个 setter

    有些情况下会隐式产生屏蔽:

    var anotherObject = { 
      a:2
    };
    var myObject = Object.create( anotherObject );
    anotherObject.a; // 2
    myObject.a; // 2
    
    anotherObject.hasOwnProperty( "a" ); // true
    myObject.hasOwnProperty( "a" ); // false
    myObject.a++; // 隐式屏蔽! 
    anotherObject.a; // 2
    myObject.a; // 3
    myObject.hasOwnProperty( "a" ); // true
    

    ++操作首先会通过 [[Prototype]] 查找属性a并从anotherObject.a获取当前属性值2,然后给这个值加1,接着用 [[Put]] 将值3赋给myObject中新建的屏蔽属性a

    所有的函数默认都会拥有一个 名为prototype的公有并且不可枚举的属性,它会指向另一个对象,这个对象通常被称为该对象的原型。

    function Foo() {
     // ...
    }
    Foo.prototype; // { }
    

    在方法射调用new时创建对象时,该对象最后会被关联到这个方法的prototype对象上。

    function Foo() { 
      // ...
    }
    var a = new Foo();
    Object.getPrototypeOf( a ) === Foo.prototype; // true
    

    new Foo()会生成一个新对象,这个新对象的内部链接[[Prototype]]关联的是 Foo.prototype对象。最后我们得到了两个对象,它们之间互相关联。

    JavaScript中,我们并不会将一个对象(“类”)复制到另一个对象(“实例”),只是将它们关联起来。这个机制通常被称为原型继承。

    构造函数

    使用new创建的对象会调用类的构造函数。

    function Foo() { 
      // ...
    }
    Foo.prototype.constructor === Foo; // true
    var a = new Foo();
    a.constructor === Foo; // true
    

    Foo.prototype默认有一个公有并且不可枚举的属性.constructor,这个属性引用的是对象关联的函数。

    可以看到通过“构造函数”调用new Foo()创建的对象也有一个.constructor属性,指向 “创建这个对象的函数”。

    函数本身并不是构造函数,然而,当你在普通的函数调用前面加上new关键字之后,就会把这个函数调用变成一个“构造函数 调用”。实际上,new会劫持所有普通函数并用构造对象的形式来调用它。

    JavaScript中对于“构造函数”最准确的解释是,所有带new的函数调用。

    如果 你创建了一个新对象并替换了函数默认的.prototype对象引用,那么新对象并不会自动获 得.constructor属性。

    function Foo() { /* .. */ }
    Foo.prototype = { /* .. */ }; // 创建一个新原型对象
    var a1 = new Foo();
    a1.constructor === Foo; // false! 
    a1.constructor === Object; // true!
    

    可以给 Foo.prototype 添加一个 .constructor 属性,不过这需要手动添加一个符
    合正常行为的不可枚举属性。

    function Foo() { /* .. */ }
    Foo.prototype = { /* .. */ }; // 创建一个新原型对象
    
    Object.defineProperty( Foo.prototype, "constructor" , {
        enumerable: false,
        writable: true,
        configurable: true,
        value: Foo // 让 .constructor 指向 Foo
    });
    

    继承

    典型的“原型风格”:

    function Foo(name) { 
      this.name = name;
    }
    Foo.prototype.myName = function() { 
      return this.name;
    };
    function Bar(name,label) { 
      Foo.call( this, name ); 
      this.label = label;
    }
    
    // 我们创建了一个新的 Bar.prototype 对象并关联到 Foo.prototype
    // 注意!现在没有 Bar.prototype.constructor 了 
    // 如果你需要这个属性的话可能需要手动修复一下它
    Bar.prototype = Object.create( Foo.prototype );
    Bar.prototype.myLabel = function() { 
      return this.label;
    };
    var a = new Bar( "a", "obj a" );
    a.myName(); // "a"
    a.myLabel(); // "obj a"
    

    ES6 开始可以直接修改现有的Bar.prototype

    Object.setPrototypeOf( Bar.prototype, Foo.prototype );
    

    检查一个实例的继承关系

    // 非常简单:b 是否出现在 c 的 [[Prototype]] 链中
    b.isPrototypeOf( c );
    
    Object.getPrototypeOf( a ) === Foo.prototype; // true
    
    // 非标准的方法访问内部 [[Prototype]] 属性
     a.__proto__ === Foo.prototype; // true
    

    写了这么多,实在写不下去了。《你不知道的js》都是满满的干货,笔记记到这里发现好多知识都非常有用,没办法省略。几下这些笔记,也是为了复习一下,以免忘得太快了,所以受益的终究还是自己呀。

    相关文章

      网友评论

        本文标题:《你不知道的js(上卷)》笔记2(this和对象原型)

        本文链接:https://www.haomeiwen.com/subject/yhsatctx.html