你不知道的JavaScript 0x2 this和对象原型——关于this

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/funkstill/article/details/88798122

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

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,I'm KYLE
speak.call(you);//Hello,I'm READER

    对this的误解

  •     把this理解为指向函数自身
function foo(num){
    console.log("foo:"+num);
    //记录foo被调用的次数
    this.count++;
}
foo.count = 0;
var i;
for(i=0;i<10;i++){
    if(i>5){
        foo(i);
    }
}
//foo:6
//foo:7
//foo:8
//foo:9
console.log(foo.count);//0

    执行foo.count =0时,的确向函数对象foo添加了一个属性count。但是函数内部代码this.count中的this并不是指向那个函数对象。实际上,创建了一个全局变量count。

function foo(num){
    console.log("foo:"+num);
    //记录foo被调用的次数
    this.count++;
}
foo.count = 0;
var i;
for(i=0;i<10;i++){
    if(i>5){
        //使用call()可以确保this指向函数对象foo本身
        foo.call(foo,i);
    }
}
//foo:6
//foo:7
//foo:8
//foo:9
console.log(foo.count);//4
  • 认为this指向函数的作用域

    这个问题有点复杂,因为在某种情况下它是正确的,但是在其他情况下它却是错误的。

    需要明确的是, this 在任何情况下都不指向函数的词法作用域。在 JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript代码访问,它存在于 JavaScript 引擎内部。

function foo() {
    var a = 2;
    this.bar();
}
function bar() {
    console.log( this.a );
}
foo(); // ReferenceError: a is not defined

     首先,这段代码试图通过 this.bar() 来引用 bar() 函数。这是绝对不可能成功的,我们之后会解释原因。调用 bar() 最自然的方法是省略前面的 this,直接使用词法引用标识符。此外,编写这段代码的开发者还试图使用 this 联通 foo() 和 bar() 的词法作用域,从而让bar() 可以访问 foo() 作用域里的变量 a。这是不可能实现的,你不能使用 this 来引用一个词法作用域内部的东西。
    每当你想要把 this 和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。

    this到底是什么

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

    绑定规则

    默认绑定

//独立函数调用
function foo(){
    console.log(this.a);
}
var a = 2;
/*foo()是直接使用不带任何修饰符的函数引用进行调用的,
因此只能使用默认绑定,无法应用其他规则,this指向全局对象*/
foo();//2  this.a被解析成全局变量a

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

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

    这里有一个微妙但是非常重要的细节,虽然 this 的绑定规则完全取决于调用位置,但是只有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 foo()的调用位置无关

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

    隐式绑定

function foo(){
    console.log(this.a);
}
/*无论是直接在 obj 中定义还是先定义再添加为引用属性,
这个函数严格来说都不属于obj 对象。然而,调用位置会使
用 obj 上下文来引用函数,因此可以说函数被调用时 obj 
对象“拥有”或者“包含”它。
*/
var obj = {
    a:2,
    foo:foo
};
obj.foo();//2

      当 foo() 被调用时,它的落脚点确实指向 obj 对象。当函数引用有上下文对象时, 隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 foo() 时 this 被绑定到 obj,因此 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 绑定到全局对象或者 undefined 上,取决于是否是严格模式。
 

function foo(){
    console.log(this.a);
}
var obj = {
    a:2,
    foo:foo
};
var bar = obj.foo;//只是引用函数本身
var a = "oops,global";
/*bar()实质是一个不带任何修饰的函数调用,
应用了默认绑定,相当于直接调用
    foo();
*/
bar();//"oops,global" 

    显式绑定

    在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。

    JavaScript 中的“所有”函数都有一些有用的特性,可以用来解决这个问题。具体点说,可以使用函数的 call(..) 和apply(..) 方法。严格来说, JavaScript 的宿主环境有时会提供一些非常特殊的函数,它们并没有这两个方法。但是这样的函数非常罕见, JavaScript 提供的绝大多数函数以及自己创建的所有函数都可以使用 call(..) 和 apply(..) 方法。

    它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象,因此称之为显式绑定。

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

     如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式。这通常被称为“装箱”。

     硬绑定

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

    硬绑定应用场景——创建一个包裹函数,传入所有的参数并返回接收到的所有值:

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

   创建一个i可以重复使用的辅助函数

function foo(something){
    console.log(this.a,something);
    return this.a+something;
}
//简单的辅助绑定函数
function bind(fn,obj){
    return function(){
        return fn.apply(obj,arguments);
    };
}
var obj = {
    a:2
};
var bar = bind(foo,obj);
var b = bar(3);//2 3
console.log(b);//5

    由于硬绑定是一种非常常用的模式,所以在 ES5 中提供了内置的方法 Function.prototype.bind 

function foo(something){
    console.log(this.a,something);
    return this.a + something;
}
var obj = {
    a:2
};
/*bind()会返回一个硬编码的新函数,它会把参数设置为
this的上下文并调用原始函数*/
var bar = foo.bind(obj);
var b = bar(3);//2 3
console.log(b);//5

     API调用的“上下文”

    第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保回调函数使用指定的 this。

function foo(el){
    console.log(el,this.id);
}
var obj = {
    id:"awesome"
};
//调用foo()时把this绑定到obj
[1,2,3].forEach(foo,obj);
//1 awesome 2 awesome 3 awesome

    new绑定

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

   使用new来调用函数,或者发生构造函数调用时,会自动执行:

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

优先级

    隐式绑定和显式绑定

function foo(){
    console.log(this.a);
}
var obj1 = {
    a:2,
    foo:foo
};
var obj2 = {
    a:3,
    foo:foo
};
obj1.foo();//2
obj2.foo();//3
obj1.foo().call(obj2);//3 显式绑定优先级更高
obj2.foo().call(obj1);//2

    new绑定和隐式绑定

function foo(something){
    this.a = something;
}
var obj1 = {
    foo:foo
};
var obj2 = {};

obj1.foo(2);
console.log(obj1.a);//2

obj1.foo.call(obj2,3);
console.log(obj2.a);//3

var bar = new obj1.foo(4);
console.log(obj1.a);//2
console.log(bar.a);//4 new绑定比隐式绑定优先级高

    new绑定和显式绑定

function foo(something){
    this.a = something;
}
var obj1 = {};

var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);//2 bar被硬绑定到obj1上

/*new 修改了硬绑定(到 obj1 的)调用 bar(..) 中的 this。
因为使用了new 绑定,得到了一个名字为 baz 的新对象,并且
 baz.a 的值是 3。*/
var baz = new bar(3);
console.log(obj1.a);//2
console.log(baz.a);//3 

   判断this:

  1. 函数是否在new中调用(newbd)?如果是,this绑定的是新创建的对象    var bar= new foo()
  2. 函数是否通过call,apply(显式绑定)或者硬绑定调用?如果是,this绑定的是指定的对象  var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。 var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。严格模式下绑定到undefined,否则绑定到全局对象。var bar = foo()

绑定例外

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

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

     更安全的this

    一种“更安全”的做法是传入一个特殊的对象,把 this 绑定到这个对象不会对程序产生任何副作用。就像网络(以及军队)一样,可以创建一个“DMZ”(demilitarizedzone, 非军事区)对象——它就是一个空的非委托的对象。

function foo(a,b){
    console.log("a:"+a+",b:"+b);
}
//DMZ空对象
var ∅ = Object.create(null);

//把数组展开成参数
foo.apply(∅,[2,3]);//a:2,b:3

//使用bind()进行柯里化
var bar = foo.bind(∅,2);
bar(3);//a:2,b:3

    间接引用

function foo(){
    console.log(this.a); 
}
var a = 2;
var o = {a:3,foo:foo};
var p = {a:4};

o.foo();//3
(p.foo = o.foo)()//2 对目标函数的引用,使用默认绑定

    箭头函数

    箭头函数不使用 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 箭头函数的绑定无法被修改

    箭头函数最常用于回调函数中,例如事件处理器或者定时器:

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

猜你喜欢

转载自blog.csdn.net/funkstill/article/details/88798122