this和对象原型

1、为什么要使用this

function identify(){
    return this.name.toUpperCase();
}
function speak(){
    var greeting = "Hello, I'm " + identify.call(this);
    console.log(greeting);
}
var me ={
    name : "Kyle"
};
var you ={
    name : "Reader"
};
identify.call(me); // KYLE
identify.call(you); // READER
speak.call(me); // Hello, 我是 KYLE
speak.call(you); // Hello, 我是 READER

这段代码可以在不同的上下文对象(me和you)中重复使用函数identify()和speak(),不用针对每个对象编写不同版本的函数。如果不用this,需要给identify()和speak()显式传入一个上下文对象。

function identify(context)
{
    return context.name.toUpperCase();
}
function speak(context)
{
    var greeting = "Hello, I'm " + identify(context);
    console.log(greeting);
}
i
dentify(you); // READER
speak(me); //hello, 我是 KYLE

随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,this提供了一种更优雅的方式来隐式传递一个对象引用,因此可以将API设计得更加简洁并且易于复用。this既不指向函数自身也不指向函数的词法作用域,实际上是在函数被调用时发生的绑定,它指向什么完全取决于和函数在哪里被调用。

2、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 的调用位置

有以下四条绑定规则:
第一:默认绑定:独立函数调用,可以把这条规则看作是无法应用其他规则时的默认规则。

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

声明在全局作用域中的变量就是全局对象的一个同名属性。本质上就是同一个东西,并不是复制得到的。在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,这时this.a被解析成了全局变量a。
第二:隐式绑定
思考下面的代码:

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

首先需要注意的是foo()声明方式,及其之后是如何被当作引用属性添加到obj中的。但是无论是直接在obj中方定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象。然而,调用位置会使用obj上下文来引用和拿书,因此可以说函数被调用时obj对象“拥有”它。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因此this.a和obj.a是一样的。对象属性引用链中只有最顶层或者最后一层会影响调用位置。

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

一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定。

function foo()
{
    console.log(this.a);
}
var obj =
{
    a : 2,
    foo : foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"

虽然bar是obj.foo的一个引用,但实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

function foo()
{
    console.log(this.a);
}
var obj =
{
    a : 2,
    foo : foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout(obj.foo, 100); // "oops, global"

JavaScript环境中内置的setTimeout()函数实现和下面的伪代码类似:
function setTimeout(fn,delay){
//等待delay毫秒
fn();<--调用位置
}
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。
第三:显示绑定
如果不想在对象内部包含函数引用,而想在某个对象上强制调用函数,可以使用函数的call()和apply()方法。它们第一个参数是一个对象,它们会把这个对象绑定到this,接着在电泳函数时指定这个this。因为直接指定this的绑定对象,因此称之为显示绑定。

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

通过foo.call(),可以在调用foo时强制把它的this绑定到obj上。如果传入一个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对像,这个原始值会被转换成它的对象形式(new String、new Boolean
)这个通常被称为“装箱”。可惜,显示绑定依然无法解决之前提出的丢失绑定问题。
但显示绑定的一个变种:硬绑定可以解决。思考下列代码:

function foo(){
    console.log(this.a);
}
var obj ={
    a : 2
};
var bar = function (){
    foo.call(obj);
};
bar(); // 2
setTimeout(bar, 100); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call(window); // 2

在bar()内部手动调用了foo.call(obj),因此强制把foo的this绑定到了obj。由于硬绑定是一种非常常用的模式,所以在ES5中提供了内置的方法Function.prototype.bind,用法如下:

function foo(something)
{
    console.log(this.a, something);
    return this.a + something;
}
var obj =
{
    a : 2
};
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5

bind()会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数。
第四:new绑定
首先需要澄清一个非常常见的关于JavaScript中函数和对象的误解。在传统的面向类的语言中,构造函数是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数。something = new MyClass();
JavaScript也有一个new操作符,使用方法看起来也和那些面向类的语言一样,然而,JavaScript中new的机制实际上和面向类的语言完全不同。在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。它们不会属于某个类,也不会实例化一个类。实际上它们只是被new操作符调用的普通函数唯一。实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a){
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a);//2

使用new来调用foo()时,会构造一个新对象并把它绑定到foo()调用中的this上。

介绍一下ES6中一种无法使用这些规则的特殊函数类型;箭头函数。
箭头函数并不是使用function关键字dinginess的,而是使用被称为“胖箭头”的操作符=>定义的。箭头函数不使用this的四种标准规则,而是根据外层(函数或者是全局)作用域来决定this。

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, 不是 3 !

foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。
箭头函数常用于回调函数中:

function foo()
{
    setTimeout(() =  >
        {
            // 这里的 this 在此法上继承自 foo()
            console.log(this.a);
        }, 100);
}
var obj =
{
    a : 2
};
foo.call(obj); // 2

箭头函数可以像bind()一样确保函数的this被绑定到指定对象。实际上在ES6之前就已经在使用一种几乎和箭头函数完全一样的模式。

function foo()
{
    var self = this; // lexical capture of this
    setTimeout(function ()
    {
        console.log(self.a);
    }, 100);
}
var obj =
{
    a : 2
};
foo.call(obj); // 2

虽然self=this和箭头函数看起来都可以取代bind(),但本质上来说,它们想替代的是this机制。

你可能感兴趣的:(this和对象原型)