走在前端的大道上
本篇将自己读过的相关 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();
其实,setTimeout
和 setInterval
都只是在全局上下文中执行一个函数而已,即使是在严格模式下:
'use strict';
function foo() {
console.log(this); // Window
}
setTimeout(foo, 1);
记住 setTimeout
和 setInterval
都只是在全局上下文中执行一个函数而已,因此 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元素:
<button onclick="alert(this.tagName.toLowerCase());">
Show this
</button>
上面的 alert 会显示 button 。注意只有外层代码中的 this 是这样设置的:
<button onclick="alert((function(){return this})());">
Show inner this
</button>
在这种情况下,没有设置内部函数的 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实例。
示例分析
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.js"></script>
<script src="https://unpkg.com/vue@2.5.9/dist/vue.js"></script>
</head>
<div id="app" style="width: 100%;height: auto;font-size:20px;">
<p id="id1"></p>
<p id="id2"></p>
</div>
<script type="text/javascript">
var message = "Hello!";
var app = new Vue({
el:"#app",
data:{
message: "你好!"
},
created: function() {
this.showMessage1(); //this 1
this.showMessage2(); //this 2
},
methods:{
showMessage1:function(){
setTimeout(function() {
document.getElementById("id1").innerText = this.message; //this 3
}, 10)
},
showMessage2:function() {
setTimeout(() => {
document.getElementById("id2").innerText = this.message; //this 4
}, 10)
}
}
});
</script>
</html>
示例定义了两个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面试题
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。