关于this这个货,常常让我感到头疼,也很难说清这货到底是什么机制,今天就详细记录一下this,了解他就跟理解闭包差不多,不理解的时候我们会感到很难受总想着避开他,当我们真正理解之后,会有种茅塞顿开的感觉,但是也不要掉以轻心,说不定哪天又给来一脚~

先看一个例子,之前的博客中也提过到的this使用:

function fn(){
console.log(this.a)
} var a = 2; var o = {a:7}; // 使用之前讲到的apply
fn.apply(o); //
fn();//

那么this那么简单好用么,当前不是,this是比较复杂的机制,有很多规则,不小心的话很会难受。

一、抛开上面的例子,对于this我们平时会有一些误解:

1.指向自身:

function fn(){
console.log(this.a);
this.a ++;
} fn.a = 0; fn(1); console.log(fn.a);//

我们预期是输出1,因为fn被调用了1次,并且那么a++ 会导致 变成1,但是最终却是0,如果下面再来一句~

var a = 0;
fn(1); console.log(a);//

this.a 实际改变的是全局作用域a,所以例子中的 this 并没有指定为所包含的这个函数当中 = =。

如果非要调用自身,可以采用具名函数的方式(不要使用arguments.callee,在上一章提到了,被废弃的方法我们还是不要接触了):

function fn(){
fn.a ++;
} fn.a = 0; fn(); fn.a;//

或者,使用apply或者call

function fn(){
this.a ++;
} fn.a = 0; fn.call(fn); fn.a;//

说一下第一个例子为毛会是0,this 在当时指向的是全局作用域,而不是函数本身,当调用fn()方法时,this.a 会在全局作用域中声明一个 a 并执行 ++,就像下面这样

function fn(){
this.a ++;
} fn(); console.log(a); // NaN (因为a只是声明,当执行RHS的时候并没有a,那么undefined+1 会是啥? NaN)

所以:this 并不是指向自身。

2.作用域

function fn(){
var a = 0;
this.fn2();
} function fn2(){
console.log(this.a) } fn(); // undefined

首先this.fn2() 确实找到了fn2,在fn2中怎么会找到a=0呢,按照之前说的,它也只是在全局作用域中创建了一个a而已(实际上都没有创建,因为此处只有LHS 没有RHS,详细了解的话到之前的文章中看一下作用域,在此调用成功就当成是个意外吧 = =)。

那么var a 把它理解为私有的属性,而在fn2中想要用fn的私有属性怎么可能呢? 这段代码实际上想通过词法作用域的概念来用来fn中的a,但this并不会查到啥,除非这样(平常使用的比较多的):

function fn(){
var a = 0;
fn2(a);
} function fn2(a){
console.log(a)
} fn();

fn2中的a通过RHS 查询在上一层找到了 0。

so、this和词法作用的查找是冲突的,不要再想着这样用了,忘了它吧~~

那么下一句你可能在很多地方都看到过:

this实际上是在函数被调用时发生的绑定,它的指向取决与函数在哪里被调用。

二、调用位置:

如何寻找位置,先告诉你,上一层或者说最后一层~ 别急着想像,看代码:

function fn(){
fn2();
} function fn2(){
console.log('fn2');
} fn();// fn2

fn的调用位置在全局作用域下,fn2的调用位置在fn下(fn2的调用栈就是 fn - > fn2)。

所以,明白了调用位置了? 如果是多层,还有一种办法通过强大的浏览器开发者界面,如下图:(多加了一个fn3,这样可能更清晰一点)

三、绑定规则:

在调用栈中找了调用位置,接下来看看绑定规则:4种

优先级为:new > 显式 > 隐式 > 默认(为啥最后说)

1.默认规则

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

这个例子上面基本上都用到了,它用到的时候默认规则,this指向的是全局对象(并不是一定指向全局对象的哦,后面会说到)

因为fn直接调用fn(),我们或许也可以这样理解 window.fn() ,this.a 指向window.a ,输出2~  是不是很好理解(有点像隐式绑定这样说~)

但是有一点是需要注意的是,严格模式下不适用

function fn(){
"use strict";
console.log(this.a);
} var a = 2; fn(); // TypeError: Cannot read property 'a' of undefined

this不会默认绑定到window上,除非~:

window.fn();//

真正意义上用到了隐式绑定~~(别急,马上就说隐式绑定)

还有很重要的一点,严格模式下默认绑定只会关注函数体内部,不会关注被谁调用,像下面这样,是可以使用的:

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

2.隐式绑定:

是否调用位置有上下文对象或者是上下文对象,或者说被某个对象包含或拥有:

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

不管是先定义或者是后引用,在此隐式绑定的规则会把函数调用中的this 绑定到这个上下文对象。(回顾一下,在这里o的fn拥有所在作用域o的闭包或者说行使权、使用权,把它看成 o = {a:2,fn:function(){ console.log(this.a) }})

在声明一次:对象引用链只有上一层或则最后一层在调用位置起作用。

function fn(){
console.log(this.a) } var o = {
a:0,
fn:fn
} var o2 = {
a:2,
o:o,
fn:o.fn
} o2.o.fn(); // 0 (绑定的是o)
o.fn();//2 (这里o2的fn为函数本身,与o没有直接关系,所以this绑定的是o2这个对象,或者说叫隐式丢失)

tip:隐式丢失

// 跟上面的例子一个意思
function fn(){
console.log(this.a)
} var o = {
a:2,
fn : fn
} var x = o.fn; x();//undefined

o.fn 引用的是函数本身,并没有执行fn函数,所以x知识引用了fn函数,当执行x函数时,应用了到了上面说到的默认绑定,(在全局作用域声明了a,但是没有赋值),好吧,怕忘记了,如果像下面这样就更清晰了

// 在上面例子的基础上加2句
var a =7;
x();//

再来一个,参考书《你不知道的javascript》:

function fn(){
console.log(this.a)
} function fn2(f){ f();
} var o = {
a:2,
fn:fn
} fn2(o.fn); // undefined

fn2执行的f  是fn函数本身,跟o没有毛关系,所以最终也是使用了默认绑定。

还有一种就是window内置对象,跟上面结果一样,在此就不写例子了。

3.显示绑定

这个可能最好理解,就是指定this要绑定的上下文对象,主要用到的就是 apply、call、bind,关于这3个货,想看的可以看看之前的文章 JS 关于 bind ,call,apply 和arguments p8

这里主要说一个概念:如果你传入一个原始值比如:“”、1、true,当作this 的绑定对象,这个值会转为它的对象形式(new String()、new Number()、new Boolean()),称为装箱。

function fn(){
console.log(this.a);
} fn.call({a:2});// fn();// undefined

如上面看到的显示绑定也不会解决丢失绑定的问题。

但是我们可以通过硬绑定来解决这个问题:

function fn(){
console.log(this.a)
} var o = {
a:2
} function fn2(){
fn.call(o);
} fn2();//2

这样调用fn2的时候都会默认显示绑定。

它的典型行为是:创建一个包裹函数,负责接收参数并返回值。

看这个例子:

function fn(f){
console.log(this.a);
console.log(f);
} var o = {
a:2
} function fn2(){
fn.apply(o,arguments)
} fn2(6);
//
//

跟上一个例子差不多,这里利用了arguments内置对象来传递参数。

还有一种是创建辅助函数:

function fn(f){
console.log(this.a);
console.log(f);
} var o = { a:3} function fn2(){ return function (){
fn.apply(o,arguments);
}
} var fn3 = fn2(); fn3(8);
//
//

对比上一个例子,一个是立即执行,另一个是返回绑定后的函数本身再进行调用。或者使用bind也行

function fn(f){
console.log(this.a);
console.log(f);
} var o = { a:3} function fn2(){
return fn.bind(o);
} var fn3 = fn2(); fn3(5);
//
//

另外关于API 调用上下文在实际应用中有需要函数上就是通过call 与apply 实现了显示绑定,比如[].forEach();

4.new 绑定

首先要说的是,new不会实例化某个类(和我之前的说法有些冲突,但是实例我们会比较好理解),因为他们是被new操作符调用的普通函数。

类似这种 new String() ,正确的说法叫做“函数调用”,因为实际上js中并不存在构造函数之说,只存在函数调用。

上面是官方一点的语言,其实我们只需要知道这几点暂时:

new 会创建一个全新的对象,并且这个对象会绑定到函数调用的this,如果这个函数没有return 那么就返回这个函数本身的新对象~

function fn(){this.a = 3;
} var fn2 = new fn(); fn2.a;//

如上,new出来的新对象fn2 绑定到了fn的this上,是一个全新的对象(这跟之前的文自定义创建对象中说到的一样,如果为私有变量,则不会拥有它,或者它看不到,但是可以使用它比如下面这种:)

function fn(){
this.a = 3;
var b = 4; this.fn2 = function(){
console.log(b) ;
}
} var fn3 = fn(); console.log(fn3);// {a: 3, fn2: ƒ}
fn3.fn2();//

这里除了this,还有闭包的相关概念,在此就不多说了。大家只要知道this绑定到了新对象上(全新的)。

好了,4种绑定说完了,接下来说下优先级:

function fn(){
console.log(this.a)
} var o = {a:2,fn:fn}; var o2 = {a:5,fn:fn} o.fn();//
o.fn.call(o1);//

那么看显示绑定应该是优先于隐式绑定的,通过最后一行可以看出来(这里我感觉有点不好理解,或者我们可以这样理解,o.fn() 是显示绑定this所以从调用位置来看,上下文o的a为2所以this.a输出的为2;o.fn 为函数本身,所以在对函数本身进行显示绑定,所以this绑定到了o2上面)

并且显示绑定和隐式绑定都会丢失this(上面提到的)。

看下一个new 绑定和隐式绑定:

function fn(f){
this.a = f
} var o = {
fn:fn
} var o2 ={} o.fn(0);
console.log(o.a);// o.fn.call(o2,3);
console.log(o2.a);// var fn2 = new o.fn(5);
console.log(o.a);//
console.log(o2.a);//
console.log(fn2.a);//

o.fn(0) 为隐式绑定,this绑定到o 上,o.a 为0 这点不用多说。

o.fn.call(o2,3); fn函数本身中this被显示绑定带o2上,o2对象获得a并为3;

最后fn2为一个new出来的新对象,this绑定到这个新对象上(上下文),它的a为5。(因为函数就是对象)

不是很明显,下面来一个(比较new 和显示绑定):

function fn(f){
this.a = f
} var o = {} var x = fn.bind(o) x(1); o.a;// var y = new x(3) y.a;//

个人感觉在判断优先级时,不能只是记住哪种规则优先级高,而是需要仔细分析,还是理解最重要

比如:

o.fn.call()    ,首先o是一个对象,o对象中包涵的函数fn 在这里并没有调用它,按照之前所说,它只是表示函数本身,那么在调用call的显示绑定并执行了该函数,那么this肯定会绑定到显示绑定的第一个参数上,所以是显示优先

var fn2 = new o.fn();  记住最关键的那句,new会创建一个新对象并绑定到this上,这样就不会迷糊了,o.fn 是函数本身,并且创建一个新对象,那么fn2 是一个全新的对象,所以这里就是new 优先

(this与call无法同时使用但是可以用bind)

var fn2 = fn.bind(o);

var bar = new fn2();

纵使怎么变,fn2是显示绑定没错,如果像上面例子 fn是这样的  function(f){ this.a = f },那么fn2.a 肯定是o的a,

但是bar 声明使用new 绑定,那么会创建一个新对象~新对象~新对象,所以,fn2里如果加上一个参数比如 new fn2(3) ,那么新声明的bar.a 肯定就是3~

不要被所谓的优先级弄晕了,记住这几条重要的规则,管它怎么变,相信都能找到最终的那个this。

这里有一个概念:第一个参数用于绑定this,剩余的参数用于传递给下层函数的这种行为被称为“部分应用”、或者“柯里化”。(bind、apply、call)

那么这里对于判断this绑定的是什么就很好查了:

1.先看new

2.再看call、apply、bind

3.看隐式调用 o.fn()

4.啥都没,那就是默认绑定(官方的语言是,如果在严格模式下,就绑定到undefined,否则绑定到全局对象

但是

如果把null 或则undefined作为this的绑定对象传给apply的话,嘿嘿~

调用的时候会被忽略~

function fn(){
console.log(this.a)
} fn.apply(null);// undefined

其实就等于直接调用 fn()罢了。

当然我们可以另类的用这种机制:

function fn(){
for(let i = 0 ;i<arguments.length;i++){
console.log(arguments[i]);
}
} fn.apply(null,[1,2]);
//
//

是的,可以用来做展开数组(但是这里看着没必要)(在es6里可以通过...来解决展开数组的问题,像这样fn(...[1,2]))

如果使用null 作为柯里化的这种操作很危险,为啥,看下面:

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

默认绑定使全局作用域的a 赋值了2(成功进行了RHS 查询)。

如果非要使用的话:可以使用空对象,如下

function fn(a){
this.a = a;
console.log(this.a)
} var n = Object.create(null); fn.call(n,2); console.log(a);// a is not defined // 或者使用严格模式也未尝不可,但是代码中混用严格模式与懒惰模式真的会很不好维护~~
function fn(a){
'use strict';
this.a = a;
} fn.call(null,2) ; // Uncaught TypeError: Cannot set property 'a' of null

拓展一下Object.create()

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 (请打开浏览器控制台以查看运行结果。

另外:间接引用也会使用默认绑定

function fn(){
console.log(this.a)
} var o = {a:2,fn:fn} var o2 = {} var x = o.fn; x();// undefined (o2.fn = o.fn)();//undefined

这里其实很简单,按照之前说的,o.fn 是函数本身,并没有进行绑定~

还有一种绑定叫做“软绑定”,可以给默认绑定指定一个全局对象和undefined的值,同事保留隐式绑定或者显示绑定this的能力~

说实话,平时我们不太会用到,了解一下就好,软绑定在内置方法中并不存在,如果想要使用,必须自己实现,下面给出官方的例子:

if(!Function.prototype.softBind){
Function.prototype.softBind = function (obj){
var fn=this;
var curried = [].slice.call(arguments,1);
var bound = function(){
return fn.apply((!this||this===(window||global))?obj:this,curried.concat.apply(curried,arguments));
};
bound.prototype = Object.create(fn.prototype);
return bound;
}; }

检查调用对象this绑定的对象到底是谁?如果是window或者undefined、null之类的那么this绑定就交给参数对象obj去处理,相反

如果不是,则交给this本身去处理,方法的最后一步把fn 也就是this的原型保留并传给新声明的还说bound并返回~

JS 关于this p9的更多相关文章

  1. Vue.js学习笔记(1)

    数据的双向绑定(ES6写法) 效果: 没有改变 input 框里面的值时

  2. js语言精粹读书笔记一

    一.语法 1.

  3. 用node-webkit(NW.js)创建桌面程序

    以往写windows桌面程序需要用MFC.C#之类的技术,那么如果你只会web开发技术呢?或者说你有一个网站,但是你想把你的网站打包成一个桌面应用程序,该如何做呢? 答案就是用node-webkit这 ...

  4. 二、JavaScript语言--JS基础--JavaScript进阶篇--浏览器对象

    1.window对象 window对象是BOM的核心,window对象指当前的浏览器窗口. window对象方法:

  5. JS原生方法实现瀑布流布局

    html部分(图片都是本地,自己需要改动图片) p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 30.0px Consolas; color: #2b7ec ...

  6. 手机浏览器JS识别

    识别方法:采用Fiddler 抓包工具 侦测手机http链接,抓取http头 查看 工具:Fiddler 1:Fiddler配置 允许远程设备连接,配置端口为默认8888(确保8888端口没有被其他进 ...

  7. js之引用类型

    一.摘要: <javascript高级程序设计第三版>一书中单独有一章对js的引用类型(Object.Array.RegExp.Function:基本包装类型:Boolean.Number ...

  8. PhotoSwipe.js 相册展示插件学习

    PhotoSwipe.js官网:http://photoswipe.com/,在这个网站上可以下载到PhotoSwipe的文件以及相关的例子. 这个组件主要是用来展示图片.相册用的,还是很实用的. 一 ...

  9. js根据条件json生成随机json:randomjson

    前端开发中,在做前后端分离的时候,经常需要手写json数据,有3个问题特别揪心: 1,数据是写死的,不能按一定的条件随机生成长度不一,内容不一的数据 2,写数组的时候,如果有很多条,需要一条一条地写, ...

随机推荐

  1. Java几种单例模式的实现与利弊

    饿汉式 提前new出来实例了,并不是在第一次调用get方法时才实例化,没有进行延迟加载 public class Singleton1 { private static Singleton1 inst ...

  2. Java Listener中Spring接口注入的使用

    在项目中使用Spring通常使用他的依赖注入可以很好的处理,接口与实现类之间的耦合性,但是通常的应用场景中都是Service层和DAO层,或者web层的话, 也是与Strust2来整合,那么如何在Li ...

  3. mybatis四大接口之 Executor

    [参考文章]:Mybatis-Executor解析 1. Executor的继承结构 2. Executor(顶层接口) 定义了执行器的一些基本操作: public interface Executo ...

  4. 网络Socket编程TCP协议例子

    1.单线程TCP服务端 public class TcpChatServer { private Integer port=8000; private ServerSocket serverSocke ...

  5. [转]cximage双缓冲绘图 .

    1.起因 本来是想用gdi绘图的,但是一想到用gdi+libpng,还要自己处理一些比如alpha的效果之类的巨麻烦(而且涉及到处理每一个像素点的计算,一般都很耗时),我对自己处理像素点的能力一直持有 ...

  6. POJ1065 Wooden Sticks(贪心+动态规划——单调递减或递增序列)

    描述 C小加有一些木棒,它们的长度和质量都已经知道,需要一个机器处理这些木棒,机器开启的时候需要耗费一个单位的时间,如果第i+1个木棒的重量和长度都大于等于 第i个处理的木棒,那么将不会耗费时间,否则 ...

  7. top 动态查看进程

    top 统计信息前五行是系统整体的统计信息 1.第一行是任务队列信息 同uptime质性命令结果一样. 06:47:11 up 6:39, 3 users, load average: 0.00, 0 ...

  8. ASP.NET MVC HtmlHelper 类的扩展方法

    再ASP.NET MVC编程中用到了R语法,在View页面编辑HTML标签的时候,ASP.NET MVC 为我们准备好了可以辅助我们写这些标签的办法,它们就是HtmlHelper.微软官方地址是:ht ...

  9. Linux cp 强制覆盖

     Linux下默认cp命令是有别名(alias cp='cp -i')的,无法强制覆盖,即使你用 -f 参数也无法强制覆盖文件,下面提供两种Linux下cp 覆盖方法. 1) 取消cp的alias,这 ...

  10. 用new关键字对一个String 变量赋值和用literal值直接赋值有什么不同(转)

    String str1="ABC": 和String str2 = new String("ABC"); 有什么区别. String str1="AB ...