前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《你不知道的js(上卷)》笔记2(this和对象原型)

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

作者头像
陨石坠灭
发布2020-01-21 16:00:26
6720
发布2020-01-21 16:00:26
举报
文章被收录于专栏:全栈之路全栈之路

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

1. 关于this

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

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

代码语言:javascript
复制
function identify() {
  return this.name.toUpperCase();
}

var me = {
  name: "Kyle"
};

identify.call( me ); // KYLE

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

代码语言:javascript
复制
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来引用函数对象。这样,更像是静态变量。

代码语言:javascript
复制
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函数对象。

代码语言:javascript
复制
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的绑定。

代码语言:javascript
复制
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 绑定规则

默认绑定

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

代码语言:javascript
复制
function foo() { 
  console.log( this.a );
}

var a = 2; 
foo(); // 2

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

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

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

代码语言:javascript
复制
function foo() {
   "use strict";
    console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

隐式绑定

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

代码语言:javascript
复制
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上,取决于是否是严格模式。

代码语言:javascript
复制
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(..)方法实现显式绑定。

代码语言:javascript
复制
function foo() { 
  console.log( this.a );
}
var obj = {
   a:2
};
foo.call( obj ); // 2

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

代码语言:javascript
复制
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绑定 > 显式绑定 > 隐式绑定 > 默认绑定

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

代码语言:javascript
复制
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. 对象

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

代码语言:javascript
复制
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

代码语言:javascript
复制
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)。

代码语言:javascript
复制
var prefix = "foo";
var myObject = {
  [prefix + "bar"]:"hello", 
  [prefix + "baz"]: "world"
};
     
myObject["foobar"]; // hello
myObject["foobaz"]; // world

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

代码语言:javascript
复制
var myArray = [ "foo", 42, "bar" ]; 
myArray["3"] = "baz"; 
myArray.length; // 4
myArray[3]; // "baz"

复制对象

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

代码语言:javascript
复制
var newObj = JSON.parse( JSON.stringify( someObj ) );

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

属性描述符

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

代码语言:javascript
复制
var myObject = { 
  a:2
};

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

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

代码语言:javascript
复制
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就可以创建一个真正的常量属性(不可修改、 重定义或者删除)

代码语言:javascript
复制
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

代码语言:javascript
复制
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

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

代码语言:javascript
复制
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可以直接应用在数组上。

代码语言:javascript
复制
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:

代码语言:javascript
复制
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]] 链。

代码语言:javascript
复制
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

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

代码语言:javascript
复制
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的公有并且不可枚举的属性,它会指向另一个对象,这个对象通常被称为该对象的原型。

代码语言:javascript
复制
function Foo() {
 // ...
}
Foo.prototype; // { }

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

代码语言:javascript
复制
function Foo() { 
  // ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true

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

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

构造函数

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

代码语言:javascript
复制
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属性。

代码语言:javascript
复制
function Foo() { /* .. */ }

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

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

代码语言:javascript
复制
function Foo() { /* .. */ }

Foo.prototype = { /* .. */ }; // 创建一个新原型对象

Object.defineProperty( Foo.prototype, "constructor" , {
    enumerable: false,
    writable: true,
    configurable: true,
    value: Foo // 让 .constructor 指向 Foo
});

继承

典型的“原型风格”:

代码语言:javascript
复制
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

代码语言:javascript
复制
Object.setPrototypeOf( Bar.prototype, Foo.prototype );

检查一个实例的继承关系

代码语言:javascript
复制
// 非常简单:b 是否出现在 c 的 [[Prototype]] 链中
b.isPrototypeOf( c );

Object.getPrototypeOf( a ) === Foo.prototype; // true

// 非标准的方法访问内部 [[Prototype]] 属性
 a.__proto__ === Foo.prototype; // true

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019/06/02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 关于this
    • 1.1 绑定规则
    • 2. 对象
    • 3. 原型
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档