资讯专栏INFORMATION COLUMN

一篇文章带你完全理解this

Jenny_Tong / 3011人阅读

摘要:追梦子追梦子通过在方法,给第一个参数添加要把添加到哪个环境中,简单来说,就会指向那个对象。追梦子追梦子还有一点就是虽然也是对象,但是在这里还是指向那个函数的实例,因为比较特殊。追梦子追梦子在严格版中的默认的不再是,而是。

走在前端的大道上

本篇将自己读过的相关 this指向 的文章中,对自己有启发的章节片段总结在这(会对原文进行删改),会不断丰富提炼总结更新。

版本一 一句话
this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象(这句话有些问题,后面会解释为什么会有问题,虽然网上大部分的文章都是这样说的,虽然在很多情况下那样去理解不会出什么问题,但是实际上那样理解是不准确的,所以在你理解this的时候会有种琢磨不透的感觉)  —— ——彻底理解js中this的指向,不必硬背。
5大规则 (1)构造函数模式的时候,this指向新生成的实例
function Aaa(name){
  this.name= name;
  this.getName=function(){
    console.log(this.name)
  }
}
var a = new Aaa("kitty");
a.getName()        //  "kitty"
var b = new Aaa("bobo");
b.getName()        //  "bobo"

如果 new 关键词出现在被调用函数的前面,那么JavaScript引擎会创建一个新的对象,被调用函数中的this指向的就是这个新创建的函数。

function ConstructorExample() {
    console.log(this);
    this.value = 10;
    console.log(this);
}

new ConstructorExample();

// -> ConstructorExample {}
// -> ConstructorExample { value: 10 }

构造函数版this:

function Fn(){
    this.user = "追梦子";
}
var a = new Fn();
console.log(a.user); //追梦子

  这里之所以对象a可以点出函数Fn里面的user是因为new关键字可以改变this的指向,将这个this指向对象a,为什么我说a是对象,因为用了new关键字就是创建一个对象实例,我们这里用变量a创建了一个Fn的实例(相当于复制了一份Fn到对象a里面),此时仅仅只是创建,并没有执行,而调用这个函数Fn的是对象a,那么this指向的自然是对象a,那么为什么对象a中会有user,因为你已经复制了一份Fn函数到对象a中,用了new关键字就等同于复制了一份。

(2)apply/call调用模式的时候,this指向apply/call方法中的第一个参数
var list1 = {name:"andy"}
var list2 = {name:"peter"}

function d(){
  console.log(this.name)
}
d.call(list1)     //  "andy" 
d.call(list2)     //  "peter" 

如果通过apply、call或者bind的方式触发函数,那么函数中的this指向传入函数的第一个参数。

function fn() {
    console.log(this);
}

var obj = {
    value: 5
};

var boundFn = fn.bind(obj);

boundFn(); // -> { value: 5 }
fn.call(obj); // -> { value: 5 }
fn.apply(obj); // -> { value: 5 }

在没有学之前,通常会有这些问题。

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user);
    }
}
var b = a.fn;
b(); //undefined

我们是想打印对象a里面的user却打印出来undefined是怎么回事呢?如果我们直接执行a.fn()是可以的。

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user);
    }
}
a.fn(); //追梦子

虽然这种方法可以达到我们的目的,但是有时候我们不得不将这个对象保存到另外的一个变量中,那么就可以通过以下方法。

1、call()

 

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user); //追梦子
    }
}
var b = a.fn;
b.call(a);

通过在call方法,给第一个参数添加要把b添加到哪个环境中,简单来说,this就会指向那个对象。

call方法除了第一个参数以外还可以添加多个参数,如下:

var a = {
    user:"追梦子",
    fn:function(e,ee){
        console.log(this.user); //追梦子
        console.log(e+ee); //3
    }
}
var b = a.fn;
b.call(a,1,2);
2、apply()

apply方法和call方法有些相似,它也可以改变this的指向

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user); //追梦子
    }
}
var b = a.fn;
b.apply(a);

同样apply也可以有多个参数,但是不同的是,第二个参数必须是一个数组,如下:

var a = {
    user:"追梦子",
    fn:function(e,ee){
        console.log(this.user); //追梦子
        console.log(e+ee); //11
    }
}
var b = a.fn;
b.apply(a,[10,1]);

或者

var a = {
    user:"追梦子",
    fn:function(e,ee){
        console.log(this.user); //追梦子
        console.log(e+ee); //520
    }
}
var b = a.fn;
var arr = [500,20];
b.apply(a,arr);

//注意如果call和apply的第一个参数写的是null,那么this指向的是window对象

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this); //Window {external: Object, chrome: Object, document: document, a: Object, speechSynthesis: SpeechSynthesis…}
    }
}
var b = a.fn;
b.apply(null);
3、bind()

bind方法和call、apply方法有些不同,但是不管怎么说它们都可以用来改变this的指向。

先来说说它们的不同吧。

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user);
    }
}
var b = a.fn;
b.bind(a);

我们发现代码没有被打印,对,这就是bind和call、apply方法的不同,实际上bind方法返回的是一个修改过后的函数。

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user);
    }
}
var b = a.fn;
var c = b.bind(a);
console.log(c); //function() { [native code] }

那么我们现在执行一下函数c看看,能不能打印出对象a里面的user

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user); //追梦子
    }
}
var b = a.fn;
var c = b.bind(a);
c();

ok,同样bind也可以有多个参数,并且参数可以执行的时候再次添加,但是要注意的是,参数是按照形参的顺序进行的。

var a = {
    user:"追梦子",
    fn:function(e,d,f){
        console.log(this.user); //追梦子
        console.log(e,d,f); //10 1 2
    }
}
var b = a.fn;
var c = b.bind(a,10);
c(1,2);

总结:call和apply都是改变上下文中的this并立即执行这个函数,bind方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加,这是它们的区别,根据自己的实际情况来选择使用。

(3)方法调用模式的时候,this指向方法所在的对象
var a={};
a.name = "hello";
a.getName = function(){
  console.log(this.name)
}
a.getName()         //"hello"

如果一个函数是某个对象的方法,并且对象使用句点符号触发函数,那么this指向的就是该函数作为那个对象的属性的对象,也就是,this指向句点左边的对象。

var obj = {
    value: 5,
    printThis: function() {
      console.log(this);
    }
};

obj.printThis(); // -> { value: 5, printThis: ƒ }

由浅入深

例子1:
function a(){
    var user = "追梦子";
    console.log(this.user); //undefined
    console.log(this); //Window
}
a();

按照我们上面说的this最终指向的是调用它的对象,这里的函数a实际是被Window对象所点出来的,下面的代码就可以证明。

function a(){
    var user = "追梦子";
    console.log(this.user); //undefined
    console.log(this);  //Window
}
window.a();

和上面代码一样吧,其实alert也是window的一个属性,也是window点出来的。

例子2:
var o = {
    user:"追梦子",
    fn:function(){
        console.log(this.user);  //追梦子
    }
}
o.fn();

  这里的this指向的是对象o,因为你调用这个fn是通过o.fn()执行的,那自然指向就是对象o,这里再次强调一点,this的指向在函数创建的时候是决定不了的,在调用的时候才能决定,谁调用的就指向谁,一定要搞清楚这个。

其实例子1和例子2说的并不够准确,下面这个例子就可以推翻上面的理论。

如果要彻底的搞懂this必须看接下来的几个例子

例子3:
var o = {
    user:"追梦子",
    fn:function(){
        console.log(this.user); //追梦子
    }
}
window.o.fn();

  这段代码和上面的那段代码几乎是一样的,但是这里的this为什么不是指向window,如果按照上面的理论,最终this指向的是调用它的对象,这里先说个而外话,window是js中的全局对象,我们创建的变量实际上是给window添加属性,所以这里可以用window点o对象。

  这里先不解释为什么上面的那段代码this为什么没有指向window,我们再来看一段代码。

var o = {
    a:10,
    b:{
        a:12,
        fn:function(){
            console.log(this.a); //12
        }
    }
}
o.b.fn();

  这里同样也是对象o点出来的,但是同样this并没有执行它,那你肯定会说我一开始说的那些不就都是错误的吗?其实也不是,只是一开始说的不准确,接下来我将补充一句话,我相信你就可以彻底的理解this的指向的问题。

  - 情况1:如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window,这里需要说明的是在js的严格版中this指向的不是window,但是我们这里不探讨严格版的问题,你想了解可以自行上网查找。

  - 情况2:如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。

  - 情况3:如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象,例子3可以证明,如果不相信,那么接下来我们继续看几个例子。

var o = {
    a:10,
    b:{
        // a:12,
        fn:function(){
            console.log(this.a); //undefined
        }
    }
}
o.b.fn();

尽管对象b中没有属性a,这个this指向的也是对象b,因为this只会指向它的上一级对象,不管这个对象中有没有this要的东西。

还有一种比较特殊的情况,例子4:
var o = {
    a:10,
    b:{
        a:12,
        fn:function(){
            console.log(this.a); //undefined
            console.log(this); //window
        }
    }
}
var j = o.b.fn;
j();

这里this指向的是window,是不是有些蒙了?其实是因为你没有理解一句话,这句话同样至关重要。

  this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的,例子4中虽然函数fn是被对象b所引用,但是在将fn赋值给变量j的时候并没有执行所以最终指向的是window,这和例子3是不一样的,例子3是直接执行了fn。

  this讲来讲去其实就是那么一回事,只不过在不同的情况下指向的会有些不同,上面的总结每个地方都有些小错误,也不能说是错误,而是在不同环境下情况就会有不同,所以我也没有办法一次解释清楚,只能你慢慢地的去体会。

(4)函数调用模式的时候,this指向window
function aa(){
  console.log(this)
}
aa()         //window

如果一个函数作为FFI被调用,意味着这个函数不符合以上任意一种调用方式,this指向全局对象,在浏览器中,即是window。

function fn() {
    console.log(this);
}

// If called in browser:
fn(); // -> Window {stop: ƒ, open: ƒ, alert: ƒ, ...}

注意,第4条规则和第3条很类似,不同的是当函数没有作为方法被调用时,它将自动隐式编程全局对象的属性——window。也就是当我们调用 fn(),可以理解为window.fn(),根据第三条规则,fn()函数中的this指向的就是window。

function fn() {
    console.log(this);
}

// In browser:
console.log(fn === window.fn); // -> true
(5) 如果出现上面对条规则的累加情况,则优先级自1至4递减,this的指向按照优先级最高的规则判断。

将规则应用于实践
看一个代码示例,并使用上面的规则判断this的指向。

var obj = {
    value: "hi",
    printThis: function() {
        console.log(this);
    }
};

var print = obj.printThis;

obj.printThis(); // -> {value: "hi", printThis: ƒ}
print(); // -> Window {stop: ƒ, open: ƒ, alert: ƒ, ...}

obj.prinThis() ,根据第三条规则this指向的就是obj。根据第四条规则print()是FFI,因此this指向window。

obj对象中printThis这一方法其实是函数的地址的一个引用,当我们将obj.printThis赋值给print时,print包含的也是函数的引用,和obj对象一点关系也没有。obj只是碰巧拥有一个指向这个函数的引用的属性。

当不适用obj对象触发函数时,这个函数就是FFI。

应用多项规则
当出现多个上述规则时,将优先级高的“获胜”,如果规则2和规则3同时存在,则规则2优先:

var obj1 = {
    value: "hi",
    print: function() {
        console.log(this);
    },
};

var obj2 = { value: 17 };

obj1.print.call(obj2); // -> { value: 17 }

如果规则1和规则3同时被应用,则规则1优先:

var obj1 = {
    value: "hi",
    print: function() {
        console.log(this);
    },
};

new obj1.print(); // -> print {}
额外的

当this碰到return时

function fn()  
{  
    this.user = "追梦子";  
    return {};  
}
var a = new fn;  
console.log(a.user); //undefined

再看一个

function fn()  
{  
    this.user = "追梦子";  
    return function(){};
}
var a = new fn;  
console.log(a.user); //undefined

再来

function fn()  
{  
    this.user = "追梦子";  
    return 1;
}
var a = new fn;  
console.log(a.user); //追梦子
function fn()  
{  
    this.user = "追梦子";  
    return undefined;
}
var a = new fn;  
console.log(a.user); //追梦子

什么意思呢?

  如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。

function fn()  
{  
    this.user = "追梦子";  
    return undefined;
}
var a = new fn;  
console.log(a); //fn {user: "追梦子"}

  还有一点就是虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊。

function fn()  
{  
    this.user = "追梦子";  
    return null;
}
var a = new fn;  
console.log(a.user); //追梦子
在严格版中的默认的this不再是window,而是undefined。 代码中引用了库?

有些库会将this的指向绑定更有用的对象上,比如jQuery库,在事件处理程序中,this的指向不是全局对象而被绑定到了元素对象上。因此,如果你发现一些不能用上述5项规则解释的情况,请阅读你所使用的库的官方文档,找到关于该库是如何改变this的指向的,通常通过 bind 方法改变this的指向。

参考文章:
1.彻底理解js中this的指向,不必硬背。
2.javascript中this指向的规则
3.The Complete Rules to "this"
4.JavaScript中的this

版本二 上下文 vs 作用域

每个函数调用都有与之相关的作用域和上下文。首先需要澄清的问题是上下文和作用域是不同的概念。很多人经常将这两个术语混淆。

作用域(scope) 是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。而上下文(context)是用来指定代码某些特定部分中 this 的值。

从根本上说,作用域是基于函数(function-based)的,而上下文是基于对象(object-based)的。换句话说,作用域是和每次函数调用时变量的访问有关,并且每次调用都是独立的。上下文总是被调用函数中关键字 this 的值,是调用当前可执行代码的对象的引用。说的通俗一点就是:this 取值,是在函数真正被调用执行的时候确定的,而不是在函数定义的时候确定的。

全局上下文

无论是否在严格模式下,在全局执行上下文中(在任何函数体外部)this 都指向全局对象。当然具体的全局对象和宿主环境有关。

在浏览器中, window 对象同时也是全局对象:

console.log(this === window); // true

NodeJS 中,则是 global 对象:

console.log(this); // global
函数上下文

由于其运行期绑定的特性,JavaScript 中的 this 含义要丰富得多,它可以是全局对象、当前对象或者任意对象,这完全取决于函数的调用方式。JavaScript 中函数的调用有以下几种方式:作为函数调用,作为对象方法调用,作为构造函数调用,和使用 apply 或 call 调用。下面我们将按照调用方式的不同,分别讨论 this 的含义

作为函数直接调用

作为函数直接调用时,要注意 2 种情况:

非严格模式

在非严格模式下执行函数调用,此时 this 默认指向全局对象。

function f1(){
  return this;
}
//在浏览器中:
f1() === window;   //在浏览器中,全局对象是window
 
//在Node中:
f1() === global;

严格模式 ‘use strict’

在严格模式下,this 将保持他进入执行上下文时的值,所以下面的 this 并不会指向全局对象,而是默认为 undefined 。

"use strict"; // 这里是严格模式
function test() {
  return this;
};
 
test() === undefined; // true
作为对象的方法调用

在 JavaScript 中,函数也是对象,因此函数可以作为一个对象的属性,此时该函数被称为该对象的方法,在使用这种调用方式时,内部的 this 指向该对象。

var Obj = {
  prop: 37,
  getProp: function() {
    return this.prop;
  }
};
 
console.log(Obj.getProp()); // 37

上面的例子中,当 Obj.getProp() 被调用时,方法内的 this 将指向 Obj 对象。值得注意的是,这种行为根本不受函数定义方式或定义位置的影响。在前面的例子中,我们在定义对象 Obj 的同时,将成员 getProp 定义了一个匿名函数。但是,我们也可以首先定义函数,然后再将其附加到 Obj.getProp 。所以,下面的代码和上面的例子是等价的:

var Obj = {
  prop: 37
};
 
function independent() {
  return this.prop;
}
 
Obj.getProp = independent;
 
console.log(Obj.getProp()); // logs 37

JavaScript 非常灵活,现在我们把对象的方法赋值给一个变量,然后直接调用这个函数变量又会发生什么呢?

var Obj = {
  prop: 37,
  getProp: function() {
    return this.prop;
  }
};
 
var test = Obj.getProp
console.log(test()); // undefined

可以看到,这时候 this 指向全局对象,这个例子 test 只是引用了 Obj.getProp 函数,也就是说这个函数并不作为 Obj 对象的方法调用,所以,它是被当作一个普通函数来直接调用。因此,this 指向全局对象。

一些坑

我们来看看下面这个例子:

var prop = 0;
var Obj = {
  prop: 37,
  getProp: function() {
    setTimeout(function() {
        console.log(this.prop) // 结果是 0 ,不是37!
    },1000)
  }
};
 
Obj.getProp();

正如你所见, setTimeout 中的 this 向了全局对象,这里不是把它当作函数的方法使用吗?这一点经常让很多初学者疑惑;这种问题是很多异步回调函数中也会普遍会碰到,通常有个土办法解决这个问题,比如,我们可以利用 闭包 的特性来处理:

var Obj = {
  prop: 37,
  getProp: function() {
    var self = this; 
    setTimeout(function() {
        console.log(self.prop) // 37
    },1000)
  }
};
 
Obj.getProp();

其实,setTimeoutsetInterval 都只是在全局上下文中执行一个函数而已,即使是在严格模式下:

"use strict";
 
function foo() {
  console.log(this); // Window
}
 
setTimeout(foo, 1);

记住 setTimeoutsetInterval 都只是在全局上下文中执行一个函数而已,因此 this 指向全局对象。 除非你实用箭头函数,Function.prototype.bind 方法等办法修复。至于解决方案会在后续的文章中继续讨论。

作为构造函数调用

JavaScript 支持面向对象式编程,与主流的面向对象式编程语言不同,JavaScript 并没有类(class)的概念,而是使用基于原型(prototype)的继承方式。作为又一项约定通用的准则,构造函数以大写字母开头,提醒调用者使用正确的方式调用。

当一个函数用作构造函数时(使用 new 关键字),它的 this 被绑定到正在构造的新对象,也就是我们常说的实例化出来的对象。

function Person(name) {
  this.name = name;
}
 
var p = new Person("愚人码头");
console.log(p.name); // "愚人码头"

几个陷阱

如果构造函数具有返回对象的 return 语句,则该返回对象将是 new 表达式的结果。

function Person(name) {
  this.name = name;
  return { title : "前端开发" };
}
 
var p = new Person("愚人码头");
console.log(p.name); // undefined
console.log(p.title); // "前端开发"

相应的,JavaScript 中的构造函数也很特殊,如果不使用 new 调用,则和普通函数一样, this 仍然执行全局:

function Person(name) {
  this.name = name;
  console.log(this); // Window 
}
 
var p = Person("愚人码头");
箭头函数中的 this

在箭头函数中,this 与封闭词法上下文的 this 保持一致,也就是说由上下文确定。

var obj = {
    x: 10,
    foo: function() {
        var fn = () => {
            return () => {
                return () => {
                    console.log(this);      //{x: 10, foo: ƒ} 即 obj
                    console.log(this.x);    //10
                }
            }
        }
        fn()()();
    }
}
obj.foo();

obj.foo 是一个匿名函数,无论如何, 这个函数中的 this 指向它被创建时的上下文(在上面的例子中,就是 obj 对象)。这同样适用于在其他函数中创建的箭头函数:这些箭头函数的this 被设置为外层执行上下文

// 创建一个含有bar方法的obj对象,bar返回一个函数,这个函数返回它自己的this,
// 这个返回的函数是以箭头函数创建的,所以它的this被永久绑定到了它外层函数的this。
// bar的值可以在调用中设置,它反过来又设置返回函数的值。
var obj = {
    bar: function() {
        var x = (() => this);
        return x;
    }
};
 
// 作为obj对象的一个方法来调用bar,把它的this绑定到obj。
// x所指向的匿名函数赋值给fn。
var fn = obj.bar();
 
// 直接调用fn而不设置this,通常(即不使用箭头函数的情况)默认为全局对象,若在严格模式则为undefined
console.log(fn() === obj); // true
 
// 但是注意,如果你只是引用obj的方法,而没有调用它(this是在函数调用过程中设置的)
var fn2 = obj.bar;
// 那么调用箭头函数后,this指向window,因为它从 bar 继承了this。
console.log(fn2()() == window); // true

在上面的例子中,一个赋值给了 obj.bar 的函数(称为匿名函数 A),返回了另一个箭头函数(称为匿名函数 B)。因此,函数B的this被永久设置为 obj.bar(函数A)被调用时的 this 。当返回的函数(函数B)被调用时,它this始终是最初设置的。在上面的代码示例中,函数B的 this 被设置为函数A的 this ,即 obj,所以它仍然设置为 obj,即使以通常将 this 设置为 undefined 或全局对象(或者如前面示例中全局执行上下文中的任何其他方法)进行调用。

填坑

我们回到上面 setTimeout 的坑:

var prop = 0;
var Obj = {
  prop: 37,
  getProp: function() {
    setTimeout(function() {
        console.log(this.prop) // 结果是 0 ,不是37!
    },1000)
  }
};
 
Obj.getProp();

通常情况我,我们在这里期望输出的结果是 37 ,用箭头函数解决这个问题相当简单:

var Obj = {
  prop: 37,
  getProp: function() {
    setTimeout(() => {
        console.log(this.prop) // 37
    },1000)
  }
};
 
Obj.getProp();
原型链中的 this

相同的概念在定义在原型链中的方法也是一致的。如果该方法存在于一个对象的原型链上,那么 this 指向的是调用这个方法的对象,就好像该方法本来就存在于这个对象上。

var o = {
  f : function(){ 
    return this.a + this.b; 
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
 
console.log(p.f()); // 5

在这个例子中,对象 p 没有属于它自己的f属性,它的f属性继承自它的原型。但是这对于最终在 o 中找到 f 属性的查找过程来说没有关系;查找过程首先从 p.f 的引用开始,所以函数中的 this 指向 p 。也就是说,因为f是作为p的方法调用的,所以它的this 指向了 p 。这是 JavaScript 的原型继承中的一个有趣的特性。

你也会看到下面这种形式的老代码,道理是一样的:

function Person(name) {
  this.name = name;
}
Person.prototype = {
  getName:function () {
    return this.name
  }
};
var p = new Person("愚人码头");
console.log(p.getName()); // "愚人码头"
getter 与 setter 中的 this

再次,相同的概念也适用时的函数作为一个 getter 或者 一个 setter 调用。用作 getter 或 setter 的函数都会把 this 绑定到正在设置或获取属性的对象。

function sum() {
  return this.a + this.b + this.c;
}
 
var o = {
  a: 1,
  b: 2,
  c: 3,
  get average() {
    return (this.a + this.b + this.c) / 3;
  }
};
 
Object.defineProperty(o, "sum", {
    get: sum, enumerable: true, configurable: true});
 
console.log(o.average, o.sum); // logs 2, 6

注:Object.defineProperty() 顾名思义,为对象定义属性,方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。是ES5的属性, 支持IE8以上。

Object.defineProperty(obj, prop, descriptor)

参数

object 必需。 要在其上添加或修改属性的对象。 这可能是一个本机 JavaScript对象(即用户定义的对象或内置对象)或 DOM 对象。

propertyname 必需。 一个包含属性名称的字符串。

descriptor 必需。 属性描述符。 它可以针对数据属性或访问器属性。

在js中我们可以通过下面这几种方法定义属性

// (1) define someOne property name
someOne.name = "cover";
//or use (2) 
someOne["name"] = "cover";
// or use (3) defineProperty
Object.defineProperty(someOne, "name", {
    value : "cover"
})

属性的状态设置
其中descriptor的参数值得我们关注下,该属性可设置的值有:
【value】 属性的值,默认为 undefined。
【writable】 该属性是否可写,如果设置成 false,则任何对该属性改写的操作都无效(但不会报错),对于像前面例子中直接在对象上定义的属性,这个属性该特性默认值为为 true。

var someOne = { };
Object.defineProperty(someOne, "name", {
    value:"coverguo" , //由于设定了writable属性为false 导致这个量不可以修改
    writable: false 
});  
console.log(someOne.name); // 输出 coverguo
someOne.name = "linkzhu";
console.log(someOne.name); // 输出coverguo

【configurable]】如果为false,则任何尝试删除目标属性或修改属性以下特性(writable, configurable, enumerable)的行为将被无效化,对于像前面例子中直接在对象上定义的属性,这个属性该特性默认值为为 true。

var someOne = { };
Object.defineProperty(someOne, "name", {
    value:"coverguo" ,
    configurable: false 
});  
delete someOne.name; 
console.log(someOne.name);// 输出 coverguo
someOne.name = "linkzhu";
console.log(someOne.name); // 输出coverguo

【enumerable】 是否能在for-in循环中遍历出来或在Object.keys中列举出来。对于像前面例子中直接在对象上定义的属性,这个属性该特性默认值为为 true。
注意 在调用Object.defineProperty()方法时,如果不指定, configurable, enumerable, writable特性的默认值都是false,这跟之前所 说的对于像前面例子中直接在对象上定义的属性,这个特性默认值为为 true并不冲突,如下代码所示:

//调用Object.defineProperty()方法时,如果不指定
var someOne = { };
someOne.name = "coverguo";
console.log(Object.getOwnPropertyDescriptor(someOne, "name"));
//输出 Object {value: "coverguo", writable: true, enumerable: true, configurable: true}

//直接在对象上定义的属性,这个特性默认值为为 true
var otherOne = {};
Object.defineProperty(otherOne, "name", {
    value:"coverguo" 
});  
console.log(Object.getOwnPropertyDescriptor(otherOne, "name"));
//输出 Object {value: "coverguo", writable: false, enumerable: false, configurable: false}

【get】一旦目标对象访问该属性,就会调用这个方法,并返回结果。默认为 undefined。
【set】 一旦目标对象设置该属性,就会调用这个方法。默认为 undefined。
从上面,可以得知,我们可以通过使用Object.defineProperty,来定义和控制一些特殊的属性,如属性是否可读,属性是否可枚举,甚至修改属性的修改器(setter)和获取器(getter)
那什么场景和地方适合使用到特殊的属性呢?

从上面,可以得知,我们可以通过使用Object.defineProperty,来定义和控制一些特殊的属性,如属性是否可读,属性是否可枚举,甚至修改属性的修改器(setter)和获取器(getter)

实际运用
在一些框架,如vue、express、qjs等,经常会看到对Object.defineProperty的使用。那这些框架是如何使用呢?

MVVM中数据‘双向绑定’实现
待补充
优化对象获取和修改属性方式
这个优化对象获取和修改属性方式,是什么意思呢? 过去我们在设置dom节点transform时是这样的。

//加入有一个目标节点, 我们想设置其位移时是这样的
var targetDom = document.getElementById("target");
var transformText = "translateX(" + 10 + "px)";
targetDom.style.webkitTransform = transformText;
targetDom.style.transform = transformText;

通过上面,可以看到如果页面是需要许多动画时,我们这样编写transform属性是十分蛋疼的。
但如果通过Object.defineProperty, 我们则可以

//这里只是简单设置下translateX的属性,其他如scale等属性可自己去尝试

Object.defineProperty(dom, "translateX", {
set: function(value) {
         var transformText = "translateX(" + value + "px)";
        dom.style.webkitTransform = transformText;
        dom.style.transform = transformText;
}
//这样再后面调用的时候, 十分简单
dom.translateX = 10;
dom.translateX = -10;
//甚至可以拓展设置如scale, originX, translateZ,等各个属性,达到下面的效果
dom.scale = 1.5;  //放大1.5倍
dom.originX = 5;  //设置中心点X
}

上面只是个简单的版本,并不是最合理的写法,但主要是为了说明具体的意图和方法
增加属性获取和修改时的信息
如在Express4.0中,该版本去除了一些旧版本的中间件,为了让用户能够更好地发现,其有下面这段代码,通过修改get属性方法,让用户调用废弃属性时抛错并带上自定义的错误信息。

[
  "json",
  "urlencoded",
  "bodyParser",
  "compress",
  "cookieSession",
  "session",
  "logger",
  "cookieParser",
  "favicon",
  "responseTime",
  "errorHandler",
  "timeout",
  "methodOverride",
  "vhost",
  "csrf",
  "directory",
  "limit",
  "multipart",
  "staticCache",
].forEach(function (name) {
  Object.defineProperty(exports, name, {
    get: function () {
      throw new Error("Most middleware (like " + name + ") is no longer bundled with Express and must be installed separately. Please see https://github.com/senchalabs/connect#middleware.");
    },
    configurable: true
  });
});
作为一个DOM事件处理函数

当函数被用作事件处理函数时,它的 this 指向触发事件的元素(一些浏览器在使用非addEventListener 的函数动态添加监听函数时不遵守这个约定)。

// 被调用时,将关联的元素变成蓝色
function bluify(e){
  console.log(this === e.currentTarget); // 总是 true
 
  // 当 currentTarget 和 target 是同一个对象是为 true
  console.log(this === e.target);        
  this.style.backgroundColor = "#A5D9F3";
}
 
// 获取文档中的所有元素的列表
var elements = document.getElementsByTagName("*");
 
// 将bluify作为元素的点击监听函数,当元素被点击时,就会变成蓝色
for(var i=0 ; i < elements.length; i++){
  elements[i].addEventListener("click", bluify, false);
}
作为一个内联事件处理函数

当代码被内联on-event 处理函数调用时,它的this指向监听器所在的DOM元素:

上面的 alert 会显示 button 。注意只有外层代码中的 this 是这样设置的:

在这种情况下,没有设置内部函数的 this,所以它指向 global/window 对象(即非严格模式下调用的函数未设置 this 时指向的默认对象)。

使用 apply 或 call 调用

JavaScript 中函数也是对象,对象则有方法,apply 和 call 就是函数对象的方法。这两个方法异常强大,他们允许切换函数执行的上下文环境(context),即 this 绑定的对象。很多 JavaScript 中的技巧以及类库都用到了该方法。让我们看一个具体的例子:

function Point(x, y){ 
   this.x = x; 
   this.y = y; 
   this.moveTo = function(x, y){ 
       this.x = x; 
       this.y = y; 
   } 
} 
 
var p1 = new Point(0, 0); 
p1.moveTo(1, 1); 
console.log(p1.x,p1.y); //1 1
 
var p2 = {x: 0, y: 0}; 
p1.moveTo.apply(p2, [10, 10]);
console.log(p2.x,p2.y); //10 10

在上面的例子中,我们使用构造函数生成了一个对象 p1,该对象同时具有 moveTo 方法;使用对象字面量创建了另一个对象 p2,我们看到使用 apply 可以将 p1 的方法 apply 到 p2 上,这时候 this 也被绑定到对象 p2 上。另一个方法 call 也具备同样功能,不同的是最后的参数不是作为一个数组统一传入,而是分开传入的:

function Point(x, y){ 
   this.x = x; 
   this.y = y; 
   this.moveTo = function(x, y){ 
       this.x = x; 
       this.y = y; 
   } 
} 
 
var p1 = new Point(0, 0); 
p1.moveTo(1, 1); 
console.log(p1.x,p1.y); //1 1
 
var p2 = {x: 0, y: 0}; 
p1.moveTo.call(p2, 10, 10); // 只是参数不同
console.log(p2.x,p2.y); //10 10
.bind() 方法

ECMAScript 5 引入了 Function.prototype.bind 。调用 f.bind(someObject) 会创建一个与 f 具有相同函数体和作用域的函数,但是在这个新函数中,this 将永久地被绑定到了 bind 的第一个参数,无论这个函数是如何被调用的。

function f(){
  return this.a;
}
 
//this被固定到了传入的对象上
var g = f.bind({a:"azerty"});
console.log(g()); // azerty
 
var h = g.bind({a:"yoo"}); //bind只生效一次!
console.log(h()); // azerty
 
var o = {a:37, f:f, g:g, h:h};
console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty

填坑

上面我们已经讲了使用箭头函数填 setTimeout 的坑,这次我们使用 bind 方法来试试:

var prop = 0;
var Obj = {
  prop: 37,
  getProp: function() {
    setTimeout(function() {
        console.log(this.prop) // 37
    }.bind(Obj),1000)
  }
};
 
Obj.getProp();

同样可以填坑,但是看上去没有使用箭头函数来的优雅。

Vue实例里this

vue文档里的原话:

All lifecycle hooks are called with their "this" context pointing to the Vue instance invoking it.

意思是:在Vue所有的生命周期钩子方法(如created,mounted, updated以及destroyed)里使用this,this指向调用它的Vue实例。

示例分析




    
    
    
    

示例定义了两个message。一个是全局变量,即window.message,它的值为英文“Hello!”。另外一个是vue实例的数据message,它的值为中文的“你好!”。

运行示例,在浏览器得到:

第一个输出英文"Hello!”,第二个输出中文“你好!”。这说明了showMessage1()里的this指的是window,而showMessage2()里的this指的是vue实例。

//created

created: function() {
  this.showMessage1();    //this 1
  this.showMessage2();   //this 2
}

created函数为vue实例的钩子方法,它里面使用的this指的是vue实例。

//showMessage1()

showMessage1:function(){
    setTimeout(function() {
       document.getElementById("id1").innerText = this.message;  //this 3
    }, 10)
}

对于普通函数(包括匿名函数),this指的是直接的调用者,在非严格模式下,如果没有直接调用者,this指的是window。showMessage1()里setTimeout使用了匿名函数,this指向window。

//showMessage2()

showMessage2:function() {
    setTimeout(() => {
       document.getElementById("id2").innerText = this.message;  //this 4
    }, 10)
}

箭头函数是没有自己的this,在它内部使用的this是由它定义的宿主对象决定。showMessage2()里定义的箭头函数宿主对象为vue实例,所以它里面使用的this指向vue实例。

绑定vue实例到this的方法

为了避免this指向出现歧义,有两种方法绑定this。

使用bind

showMessage1()可以改为:

showMessage1:function(){
    setTimeout(function() {
       document.getElementById("id1").innerText = this.message;  //this 3
    }.bind(this), 10)
}

对setTimeout()里的匿名函数使用bind()绑定到vue实例的this。这样在匿名函数内的this也为vue实例。

赋值给另一个变量

showMessage1()也可以改为

showMessage1:function(){
    var self = this;
    setTimeout(function() {
       document.getElementById("id1").innerText = self.message;  //改为self
    }.bind(this), 10)
}

这里吧表示vue实例的this赋值给变量self。在使用到this的地方改用self引用。

参考文章:
1.全面理解 JavaScript 中的 this
2.不会Object.defineProperty你就out了
3.10道典型的JavaScript面试题

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90281.html

相关文章

  • 手挽手带你学React:二档 React生命周期以及组件开发

    摘要:手挽手带你学入门二档组件开发的开始,合理运用生命周期和组件,能够让你的开发变地流利又这篇文章带你学会创建组件,运用组建。 手挽手带你学React入门二档,组件开发的开始,合理运用生命周期和组件,能够让你的开发变地流利又happy,这篇文章带你学会创建组件,运用组建。学起来吧! React 组件生命周期 学习React,生命周期很重要,我们了解完生命周期的各个组件,对写高性能组件会有很大...

    izhuhaodev 评论0 收藏0
  • 文章带你理解原型和原型链

    摘要:上面的代码,运行以后,我们可以看到因为的原型是指向的实例上的,所以可以访问他的属性值,那如果我不想让访问的构造函数里声明的属性值,那怎么办呢只需要将指向的原型而不是实例就行了。 走在前端的大道上 本篇将自己读过的相关 javascript原型和原型链 文章中,对自己有启发的章节片段总结在这(会对原文进行删改),会不断丰富提炼总结更新。 文章——深入理解javascript之原型 一般的...

    yintaolaowanzi 评论0 收藏0
  • 图文 视频双管齐下,带你全面彻底理解Retrofit源码,Android开发五年

    摘要:协程的判断条件下面我们来着重看下的源码,因为从这里开始就涉及到协程的判断。第二点是关键点,用来判断该方法的调用是否使用到了协程。原理我们先来看下使用协程是怎么写的这是一个标准的协程写法,然后我们再套用上面的条件,发现完全匹配不到。 第一眼看,跟我之前印象中的有点区别(也不知道是什么版本),return的时候居然...

    不知名网友 评论0 收藏0
  • 文章带你了解如何用Planting 为测试工程师开发的部署框架

    摘要:是一个为测试工程师开发的部署框架,使用语言编写,为了解决测试团队在测试过程中的部署问题。部署执行方式简单,支持命令行与自动化测试可紧密合作作为一个为测试工程师开发的部署框架,通过命令行进行自动化部署是第一选择。 ...

    yiliang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<