js 的 this 绑定问题,让多数新手懵逼,部分老手以为恶心,这是由于this的绑定 ‘难以捉摸’,出错的时候还每每不知道为何,至关反逻辑。
让咱们考虑下面代码:面试
var people = { name : "海洋饼干", getName : function(){ console.log(this.name); } }; window.onload = function(){ xxx.onclick = people.getName; };
在平时搬砖时比较常见的this绑定问题,你们可能也写给或者遇到过,当xxx.onclick触发时,输出什么呢 ?数组
为了方便测试,我将代码简化:app
var people = { Name: "海洋饼干", getName : function(){ console.log(this.Name); } }; var bar = people.getName; bar(); // undefined
经过这个小例子带你们感觉一下this
恶心的地方,我最开始遇到这个问题的时候也是一脸懵逼,由于代码里的this
在建立时指向很是明显啊,指向本身 people
对象,可是实际上指向 window
对象,这就是我立刻要和你们说的 this
绑定规则。函数
this
什么是this
?在讨论this
绑定前,咱们得先搞清楚this表明什么。测试
this
绑定规则掌握了下面介绍的4种绑定的规则,那么你只要看到函数调用就能够判断 this
的指向了。this
考虑下面代码:prototype
function foo(){ var a = 1 ; console.log(this.a); // 10 } var a = 10; foo();
这种就是典型的默认绑定,咱们看看foo调用的位置,”光杆司令“,像 这种直接使用而不带任何修饰的函数调用 ,就 默认且只能 应用 默认绑定。code
那默认绑定到哪呢,通常是window
上,严格模式下 是undefined
。对象
代码说话:继承
function foo(){ console.log(this.a); } var obj = { a : 10, foo : foo } foo(); // ? obj.foo(); // ?
答案 : undefined 10
foo()
的这个写法熟悉吗,就是咱们刚刚写的默认绑定,等价于打印window.a
,故输出undefined
,
下面obj.foo()
这种你们应该常常写,这其实就是咱们立刻要讨论的 隐性绑定 。
函数foo执行的时候有了上下文对象,即 obj
。这种状况下,函数里的this默认绑定为上下文对象,等价于打印obj.a
,故输出10
。
若是是链性的关系,好比 xx.yy.obj.foo();
, 上下文取函数的直接上级,即紧挨着的那个,或者说对象链的最后一个。
在咱们刚刚的 隐性绑定中有一个致命的限制,就是上下文必须包含咱们的函数 ,例:var obj = { foo : foo }
,若是上下文不包含咱们的函数用隐性绑定明显是要出错的,不可能每一个对象都要加这个函数 ,那样的话扩展,维护性太差了,咱们接下来聊的就是直接 给函数强制性绑定this。
这里咱们就要用到 js 给咱们提供的函数 call 和 apply,它们的做用都是改变函数的this指向,第一个参数都是 设置this对象。
两个函数的区别:
例如:
function foo(a,b){ console.log(a+b); } foo.call(null,'海洋','饼干'); // 海洋饼干 这里this指向不重要就写null了 foo.apply(null, ['海洋','饼干'] ); // 海洋饼干
除了 call,apply函数之外,还有一个改变this的函数 bind ,它和call,apply都不一样。
bind只有一个函数,且不会马上执行,只是将一个值绑定到函数的this上,并将绑定好的函数返回。例:
function foo(){ console.log(this.a); } var obj = { a : 10 }; foo = foo.bind(obj); foo(); // 10
(bind函数很是特别,下次和你们一块儿讨论它的源码)
开始正题,上代码,就用上面隐性绑定的例子 :
function foo(){ console.log(this.a); } var obj = { a : 10 //去掉里面的foo } foo.call(obj); // 10
咱们将隐性绑定例子中的 上下文对象 里的函数去掉了,显然如今不能用 上下文.函数
这种形式来调用函数,你们看代码里的显性绑定代码foo.call(obj)
,看起来很怪,和咱们以前所了解的函数调用不同。
其实call 是 foo 上的一个函数,在改变this指向的同时执行这个函数。
(想要深刻理解 [call apply bind this硬绑定,软绑定,箭头函数绑定
] 等更多黑科技 的小伙伴欢迎关注我或本文的评论,最近我会单独作一期放到一块儿写一篇文章)(不想看的小伙伴不用担忧,不影响对本文的理解)
new
学过面向对象的小伙伴对new确定不陌生,js的new和传统的面向对象语言的new的做用都是建立一个新的对象,可是他们的机制彻底不一样。
建立一个新对象少不了一个概念,那就是构造函数
,传统的面向对象 构造函数 是类里的一种特殊函数,要建立对象时使用new 类名()
的形式去调用类中的构造函数,而js中就不同了。
js中的只要用new修饰的 函数就是'构造函数',准确来讲是 函数的构造调用
,由于在js中并不存在所谓的'构造函数'。
那么用new 作到函数的构造调用
后,js帮咱们作了什么工做呢:
__proto__
属性指向 原函数的prototype
属性。(即继承原函数的原型)第三条就是咱们下面要聊的new绑定
不哔哔,看代码:
function foo(){ this.a = 10; console.log(this); } foo(); // window对象 console.log(window.a); // 10 默认绑定 var obj = new foo(); // foo{ a : 10 } 建立的新对象的默认名为函数名 // 而后等价于 foo { a : 10 }; var obj = foo; console.log(obj.a); // 10 new绑定
使用new调用函数后,函数会 以本身的名字 命名 和 建立 一个新的对象,并返回。
特别注意 : 若是原函数返回一个对象类型,那么将没法返回新对象,你将丢失绑定this的新对象,例:
function foo(){ this.a = 10; return new String("捣蛋鬼"); } var obj = new foo(); console.log(obj.a); // undefined console.log(obj); // "捣蛋鬼"
过程是些无聊的代码测试,我直接写出优先级了
new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
若是函数被new
修饰
this绑定的是新建立的对象,例:var bar = new foo(); 函数 foo 中的 this 就是一个叫foo的新建立的对象 , 而后将这个对象赋给bar , 这样的绑定方式叫 new绑定 .
若是函数是使用call,apply,bind
来调用的
this绑定的是 call,apply,bind 的第一个参数.例: foo.call(obj); , foo 中的 this 就是 obj , 这样的绑定方式叫 显性绑定 .
若是函数是在某个 上下文对象 下被调用
this绑定的是那个上下文对象,例 : var obj = { foo : foo }; obj.foo(); foo 中的 this 就是 obj . 这样的绑定方式叫 隐性绑定 .
若是都不是,即便用默认绑定
例:function foo(){...} foo() ,foo 中的 this 就是 window.(严格模式下默认绑定到undefined). 这样的绑定方式叫 默认绑定 .
1.
var x = 10; var obj = { x: 20, f: function(){ console.log(this.x); // ? var foo = function(){ console.log(this.x); } foo(); // ? } }; obj.f();
-----------------------答案---------------------
答案 : 20 10
解析 :考点 1. this默认绑定 2. this隐性绑定
var x = 10; var obj = { x: 20, f: function(){ console.log(this.x); // 20 // 典型的隐性绑定,这里 f 的this指向上下文 obj ,即输出 20 function foo(){ console.log(this.x); } foo(); // 10 //有些人在这个地方就想固然的以为 foo 在函数 f 里,也在 f 里执行, //那 this 确定是指向obj 啊 , 仔细看看咱们说的this绑定规则 , 对应一下很容易 //发现这种'光杆司令',是咱们一开始就示范的默认绑定,这里this绑定的是window } }; obj.f();
2.
function foo(arg){ this.a = arg; return this }; var a = foo(1); var b = foo(10); console.log(a.a); // ? console.log(b.a); // ?
-----------------------答案---------------------
答案 : undefined 10
解析 :考点 1. 全局污染 2. this默认绑定
这道题颇有意思,问题基本上都集中在第一undefined上,这实际上是题目的小陷阱,可是追栈的过程绝对精彩
让咱们一步步分析这里发生了什么:
本题中全部变量的值,a = window.a = 10 , a.a = undefined , b = window , b.a = window.a = 10;
3.
var x = 10; var obj = { x: 20, f: function(){ console.log(this.x); } }; var bar = obj.f; var obj2 = { x: 30, f: obj.f } obj.f(); bar(); obj2.f();
-----------------------答案---------------------
答案:20 10 30
解析:传说中的送分题,考点,辨别this绑定
var x = 10; var obj = { x: 20, f: function(){ console.log(this.x); } }; var bar = obj.f; var obj2 = { x: 30, f: obj.f } obj.f(); // 20 //有上下文,this为obj,隐性绑定 bar(); // 10 //'光杆司令' 默认绑定 ( obj.f 只是普通的赋值操做 ) obj2.f(); //30 //无论 f 函数怎么折腾,this只和 执行位置和方式有关,即咱们所说的绑定规则
4. 压轴题了
function foo() { getName = function () { console.log (1); }; return this; } foo.getName = function () { console.log(2);}; foo.prototype.getName = function () { console.log(3);}; var getName = function () { console.log(4);}; function getName () { console.log(5);} foo.getName (); // ? getName (); // ? foo().getName (); // ? getName (); // ? new foo.getName (); // ? new foo().getName (); // ? new new foo().getName (); // ?
-----------------------答案---------------------
答案:2 4 1 1 2 3 3
解析:考点 1. new绑定 2.隐性绑定 3. 默认绑定 4.变量污染
function foo() { getName = function () { console.log (1); }; //这里的getName 将建立到全局window上 return this; } foo.getName = function () { console.log(2);}; //这个getName和上面的不一样,是直接添加到foo上的 foo.prototype.getName = function () { console.log(3);}; // 这个getName直接添加到foo的原型上,在用new建立新对象时将直接添加到新对象上 var getName = function () { console.log(4);}; // 和foo函数里的getName同样, 将建立到全局window上 function getName () { console.log(5);} // 同上,可是这个函数不会被使用,由于函数声明的提高优先级最高,因此上面的函数表达式将永远替换 // 这个同名函数,除非在函数表达式赋值前去调用getName(),可是在本题中,函数调用都在函数表达式 // 以后,因此这个函数能够忽略了 // 经过上面对 getName的分析基本上答案已经出来了 foo.getName (); // 2 // 下面为了方便,我就使用输出值来简称每一个getName函数 // 这里有小伙伴疑惑是在 2 和 3 之间,以为应该是3 , 但其实直接设置 // foo.prototype上的属性,对当前这个对象的属性是没有影响的,若是要使 // 用的话,能够foo.prototype.getName() 这样调用 ,这里须要知道的是 // 3 并不会覆盖 2,二者不冲突 ( 当你使用new 建立对象时,这里的 // Prototype 将自动绑定到新对象上,即用new 构造调用的第二个做用) getName (); // 4 // 这里涉及到函数提高的问题,不知道的小伙伴只须要知道 5 会被 4 覆盖, // 虽然 5 在 4 的下面,其实 js 并非彻底的自上而下,想要深刻了解的 // 小伙伴能够看文章最后的连接 foo().getName (); // 1 // 这里的foo函数执行完成了两件事, 1. 将window.getName设置为1, // 2. 返回window , 故等价于 window.getName(); 输出 1 getName (); // 1 // 刚刚上面的函数刚把window.getName设置为1,故同上 输出 1 new foo.getName (); // 2 // new 对一个函数进行构造调用 , 即 foo.getName ,构造调用也是调用啊 // 该执行仍是执行,而后返回一个新对象,输出 2 (虽然这里没有接收新 // 建立的对象可是咱们能够猜到,是一个函数名为 foo.getName 的对象 // 且__proto__属性里有一个getName函数,是上面设置的 3 函数) new foo().getName (); // 3 // 这里特别的地方就来了,new 是对一个函数进行构造调用,它直接找到了离它 // 最近的函数,foo(),并返回了应该新对象,等价于 var obj = new foo(); // obj.getName(); 这样就很清晰了,输出的是以前绑定到prototype上的 // 那个getName 3 ,由于使用new后会将函数的prototype继承给 新对象 new new foo().getName (); // 3 // 哈哈,这个看上去很吓人,让咱们来分解一下: // var obj = new foo(); // var obj1 = new obj.getName(); // 好了,仔细看看, 这不就是上两题的合体吗,obj 有getName 3, 即输出3 // obj 是一个函数名为 foo的对象,obj1是一个函数名为obj.getName的对象
箭头函数,一种特殊的函数,不使用function
关键字,而是使用=>
,学名 胖箭头
(2333),它和普通函数的区别:
先看个代码巩固一下:
function foo(){ return ()=>{ console.log(this.a); } } foo.a = 10; // 1. 箭头函数关联父级做用域this var bar = foo(); // foo默认绑定 bar(); // undefined 哈哈,是否是有小伙伴想固然了 var baz = foo.call(foo); // foo 显性绑定 baz(); // 10 // 2. 箭头函数this不可修改 //这里咱们使用上面的已经绑定了foo 的 baz var obj = { a : 999 } baz.call(obj); // 10
来来来,实战一下,还记得咱们以前第一个例子吗,将它改为箭头函数的形式(能够完全解决恶心的this绑定问题):
var people = { Name: "海洋饼干", getName : function(){ console.log(this.Name); } }; var bar = people.getName; bar(); // undefined
====================修改后====================
var people = { Name: "海洋饼干", getName : function(){ return ()=>{ console.log(this.Name); } } }; var bar = people.getName(); //得到一个永远指向people的函数,不用想this了,岂不是美滋滋? bar(); // 海洋饼干
可能会有人不解为何在箭头函数外面再套一层,直接写不就好了吗,搞这么麻烦干吗,其实这也是箭头函数不少人用很差的地方
var obj= { that : this, bar : function(){ return ()=>{ console.log(this); } }, baz : ()=>{ console.log(this); } } console.log(obj.that); // window obj.bar()(); // obj obj.baz(); // window
美滋滋,溜了溜了
参考书籍:你不知道的JavaScript<上卷> KYLE SIMPSON 著 (推荐)