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

书籍封面

学了多种语言,发现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》都是满满的干货,笔记记到这里发现好多知识都非常有用,没办法省略。几下这些笔记,也是为了复习一下,以免忘得太快了,所以受益的终究还是自己呀。

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

推荐阅读更多精彩内容