关于javascript:细说Js中的this

6次阅读

共计 10788 个字符,预计需要花费 27 分钟才能阅读完成。

为什么应用 this

先看个例子:

function identity() {return this.name.toUpperCase();
}

function speak() {return "Hello, i'm " + identity.call(this);
}

var me = {name: 'rod chen'}

var you = {name: "others in Aug"}

console.log(identity.call(me));  //ROD CHEN
console.log(identity.call(you)); //OTHERS IN AUG

console.log(speak.call(me));     //Hello, i'm ROD CHEN  
console.log(speak.call(you));    //Hello, i'm OTHERS IN AUG

输入的后果很显著,对于 call 的用法后面文章有提到,第一个参数就是传入到函数里的 this 的值。这段代码能够在不同的上下文对象(me 和 you)中重复使用函数 identify() 和 speak(),如果咱们不实用 this 的话,那就须要 identity 和 speak 显示传入一个上下文对象,就像上面的形式

function identity(context) {return context.name.toUpperCase();
}

function speak(context) {return "Hello, i'm " + identity(context);
}

var me = {name: 'rod chen'}

var you = {name: "others in Aug"}

console.log(identity(me));
console.log(identity(you));

console.log(speak(me));
console.log(speak(you));

总结:
this 提供了一种更优雅的形式来隐式“传递”一个对象援用,因而能够将 API 设计得更加简洁并且易于复用。随着应用模式越来越简单,显式传递上下文对象会让代码变得越来越凌乱,应用 this 则不会这样

Reference

ECMAScript 的类型分为语言类型和标准类型。

ECMAScript 语言类型是开发者间接应用 ECMAScript 能够操作的。其实就是咱们常说的 Undefined, Null, Boolean, String, Number, 和 Object。

而标准类型相当于 meta-values,是用来用算法形容 ECMAScript 语言构造和 ECMAScript 语言类型的。标准类型包含:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。

没懂?没关系,咱们只有晓得在 ECMAScript 标准中还有一种只存在于标准中的类型,它们的作用是用来描述语言底层行为逻辑。

Reference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的

这里的 Reference 是一个 Specification Type,也就是“只存在于标准里的形象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于理论的 js 代码中。

组成

这段讲述了 Reference 的形成,由三个组成部分,别离是:

  • base value
  • referenced name
  • strict reference

可是这些到底是什么呢?

咱们简略的了解的话:

base value 就是属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种。

referenced name 就是属性的名称。

var foo = 1;

// 对应的 Reference 是:var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};
var foo = {bar: function () {return this;}
};

foo.bar(); // foo

// bar 对应的 Reference 是:var BarReference = {
    base: foo,
    propertyName: 'bar',
    strict: false
};

办法

  • GetBase:返回 reference 的 base value。
  • IsPropertyReference:简略的了解:如果 base value 是一个对象,就返回 true。
  • GetValue:返回具体的值

如何确定 this 的值

function call 步骤

  1. 令 ref 为解释执行 MemberExpression 的后果 .
  2. 令 func 为 GetValue(ref).
  3. 令 argList 为解释执行 Arguments 的后果 , 产生参数值们的外部列表 (see 11.2.4).
  4. 如果 Type(func) is not Object,抛出一个 TypeError 异样 .
  5. 如果 IsCallable(func) is false,抛出一个 TypeError 异样 .
  6. 如果 Type(ref) 为 Reference,那么 如果 IsPropertyReference(ref) 为 true,那么 令 thisValue 为 GetBase(ref). 否则 , ref 的基值是一个环境记录项 令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的后果
  7. 否则 , 如果 Type(ref) 不是 Reference. 令 thisValue 为 undefined.
  8. 返回调用 func 的 [[Call]] 内置办法的后果 , 传入 thisValue 作为 this 值和列表 argList 作为参数列表

MemberExpression

  • PrimaryExpression // 原始表达式 能够参见《JavaScript 权威指南第四章》
  • FunctionExpression // 函数定义表达式
  • MemberExpression [Expression] // 属性拜访表达式
  • MemberExpression . IdentifierName // 属性拜访表达式
  • new MemberExpression Arguments // 对象创立表达式

这里说的是办法调用的右边局部。

function foo() {console.log(this)
}

foo(); // MemberExpression 是 foo

function foo() {return function() {console.log(this)
    }
}

foo()(); // MemberExpression 是 foo()

var foo = {bar: function () {return this;}
}

foo.bar(); // MemberExpression 是 foo.bar

判断 ref 的类型

第一步计算 ref,第七步须要判断 ref 是不是一个 reference 类型。

var value = 1;

var foo = {
  value: 2,
  bar: function () {return this.value;}
}

// 示例 1
console.log(foo.bar());
// 示例 2
console.log((foo.bar)());
// 示例 3
console.log((foo.bar = foo.bar)());
// 示例 4
console.log((false || foo.bar)());
// 示例 5
console.log((foo.bar, foo.bar)());

foo.bar()
这个是属性拜访。依据 11.2.1 property accessors 最初一步:

Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict.

返回了一个 reference 类型:返回一个 reference 类型的援用,其基值为 baseValue 且其援用名为 propertyNameString, 严格模式标记为 strict

var Reference = {
  base: foo,
  name: 'bar',
  strict: false
};

而后回到 function call 第六、七步,

  • 如果 Type(ref) 为 Reference,那么 如果 IsPropertyReference(ref) 为 true,那么 令 thisValue 为 GetBase(ref). 否则 , ref 的基值是一个环境记录项 令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的后果
  • 否则 , 如果 Type(ref) 不是 Reference. 令 thisValue 为 undefined.

这里因为 foo 是一个对象,所以这里 IsPropertyReference(ref) 的值为 true。所以这里 this 的值就是 GetBase(ref)就是 foo。参考视频解说:进入学习

(foo.bar)()
分组表达式规定如下:

  1. 返回执行 Expression 的后果,它可能是 Reference 类型

这里的后果和下面雷同都是 fo。

(foo.bar = foo.bar)()
波及到简略赋值,规定如下:

  1. 令 lref 为解释执行 LeftH 和 SideExpression 的后果 .
  2. 令 rref 为解释执行 AssignmentExpression 的后果 .
  3. 令 rval 为 GetValue(rref).
  4. 抛出一个 SyntaxError 异样,当以下条件都成立 :

    • Type(lref) 为 Reference
    • IsStrictReference(lref) 为 true
    • Type(GetBase(lref)) 为环境记录项
    • GetReferencedName(lref) 为 “eval” 或 “arguments”
  5. 调用 PutValue(lref, rval).
  6. 返回 rval.

这里的返回值为第三部,GetValue。这是一个具体的返回值。依据下面的值,失去 this 的值为 undefined,非严格模式下这里隐式装换为 window 对象。

(false || foo.bar)()和 (foo.bar, foo.bar)()
这里的返回值都是去的 getValue 的值。所以 this 都和下面一样。

看一下最终的后果:

var value = 1;

var foo = {
  value: 2,
  bar: function () {return this.value;}
}

// 示例 1
console.log(foo.bar()); // 2
// 示例 2
console.log((foo.bar)()); // 2
// 示例 3
console.log((foo.bar = foo.bar)()); // 1
// 示例 4
console.log((false || foo.bar)()); // 1
// 示例 5
console.log((foo.bar, foo.bar)()); // 1

一般函数

function foo() {console.log(this)
}

foo(); 

这种属于解析标识符

The result of evaluating an identifier is always a value of type Reference with its referenced name component equal to the Identifier String.

解释执行一个标识符失去的后果必然是 援用 类型的对象,且其援用名属性的值与 Identifier 字符串相等。

那么 baseValue 是什么值呢?因为解析标识符会调用的后果是 GetIdentifierReference。

  1. 如果 lex 的值为 null,则:

    1. 返回一个类型为 援用 的对象,其基值为 undefined,援用的名称为 name,严格模式标识的值为 strict。
  2. 令 envRec 为 lex 的环境数据。
  3. 以 name 为参数 N,调用 envRec 的 HasBinding(N) 具体方法,并令 exists 为调用的后果。
  4. 如果 exists 为 true,则:

    1. 返回一个类型为 援用 的对象,其基值为 envRec,援用的名称为 name,严格模式标识的值为 strict。
  5. 否则:

    1. 令 outer 为 lex 的 外部环境援用。
    2. 以 outer、name 和 struct 为参数,调用 GetIdentifierReference,并返回调用的后果。

这里因为是 window 对象所以这里返回的是:

var fooReference = {
    base: EnvironmentRecord,  // 或者这里是 undefined
    name: 'foo',
    strict: false
};

不论 base 的值是下面两种的哪一种,那都不是 Object。那依据下面的规定:

  • 如果 Type(ref) 为 Reference,那么 如果 IsPropertyReference(ref) 为 true,那么 令 thisValue 为 GetBase(ref). 否则 , ref 的基值是一个环境记录项 令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的后果
  • 否则 , 如果 Type(ref) 不是 Reference. 令 thisValue 为 undefined.

this 的值为 ImplicitThisValue 的值。这个值返回的始终 undefined。所以这里 this 的是 undefined。

什么是 this

说的是执行上下文的 thisbinding。
this 说的是以后函数的调用地位。这个是概念形容。上面通过下面的只是去剖析各种状况下的 thisbinding 是什么货色。

this 提供了一种更优雅的形式来隐式“传递”一个对象援用,因而能够将 API 设计得更加简洁
并且易于复用。随着应用模式越来越简单,显式传递上下文对象会让代码变得越来越凌乱,应用 this 则不会这样

函数调用

具体参照下面说的一般函数

call,apply

var person = {
  name: "axuebin",
  age: 25
};
function say(job){console.log(this.name+":"+this.age+" "+job);
}
say.call(person,"FE"); // axuebin:25
say.apply(person,["FE"]); // axuebin:25

call

Function.prototype.call (thisArg [ , arg1 [ , arg2, …] ] ) 当以 thisArg 和可选的 arg1, arg2 等等作为参数在一个 func 对象上调用 call 办法,采纳如下步骤:

  1. 如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异样。
  2. 令 argList 为一个空列表。
  3. 如果调用这个办法的参数多余一个,则从 arg1 开始以从左到右的程序将每个参数插入为 argList 的最初一个元素。
  4. 提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 外部办法,返回后果。

call 办法的 length 属性是 1。

this 的值为传入 thisArg 的值。

apply

  1. 如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异样
  2. 如果 argArray 是 null 或 undefined, 则

    1. 返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 外部办法的后果。
  3. 如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异样 .
  4. 令 len 为以 “length” 作为参数调用 argArray 的 [[Get]] 外部办法的后果。
  5. 令 n 为 ToUint32(len).
  6. 令 argList 为一个空列表 .
  7. 令 index 为 0.
  8. 只有 index < n 就反复

    1. 令 indexName 为 ToString(index).
    2. 令 nextArg 为以 indexName 作为参数调用 argArray 的 [[Get]] 外部办法的后果。
    3. 将 nextArg 作为最初一个元素插入到 argList 里。
    4. 设定 index 为 index + 1.
  9. 提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 外部办法,返回后果。

apply 办法的 length 属性是 2。

留神 在里面传入的 thisArg 值会批改并成为 this 值。thisArg 是 undefined 或 null 时它会被替换成全局对象,所有其余值会被利用 ToObject 并将后果作为 this 值,这是第三版引入的更改。

function [[call]]

因为下面说了调用 function 的 [[Call]] 外部办法。
当用一个 this 值,一个参数列表调用函数对象 F 的 [[Call]] 外部办法,采纳以下步骤:

  1. 用 F 的 [[FormalParameters]] 外部属性值,参数列表 args,10.4.3 形容的 this 值来建设 函数代码 的一个新执行环境,令 funcCtx 为其后果。
  2. 令 result 为 FunctionBody(也就是 F 的 [[Code]] 外部属性)解释执行的后果。如果 F 没有 [[Code]] 外部属性或其值是空的 FunctionBody,则 result 是 (normal, undefined, empty)。
  3. 退出 funcCtx 执行环境,复原到之前的执行环境。
  4. 如果 result.type 是 throw 则抛出 result.value。
  5. 如果 result.type 是 return 则返回 result.value。
  6. 否则 result.type 必然是 normal。返回 undefined。

这里又提到了 10.4.3 的执行函数代码的规定:

当控制流依据一个函数对象 F、调用者提供的 thisArg 以及调用者提供的 argumentList,进入 函数代码 的执行环境时,执行以下步骤:

  1. 如果 函数代码 是 严格模式下的代码,设 this 绑定为 thisArg。
  2. 否则如果 thisArg 是 null 或 undefined,则设 this 绑定为 全局对象。
  3. 否则如果 Type(thisArg) 的后果不为 Object,则设 this 绑定为 ToObject(thisArg)。
  4. 否则设 this 绑定为 thisArg。
  5. 以 F 的 [[Scope]] 外部属性为参数调用 NewDeclarativeEnvironment,并令 localEnv 为调用的后果。
  6. 设词法环境为 localEnv。
  7. 设变量环境为 localEnv。
  8. 令 code 为 F 的 [[Code]] 外部属性的值。
  9. 按 10.5 形容的计划,应用 函数代码 code 和 argumentList 执行定义绑定初始化步骤。

bind

var person = {
  name: "axuebin",
  age: 25
};
function say(){console.log(this.name+":"+this.age);
}
var f = say.bind(person);
console.log(f());

外面同样的也是讲传入的 thisArg 设置 this 的值。

箭头函数

箭头函数并不绑定 this,arguments,super(ES6),或者 new.target(ES6),这些常识沿用蕴含以后箭头函数的关闭的词法环境。

function foo() {setTimeout( () => {console.log("args:", arguments);
   },100);
}

foo(2, 4, 6, 8);
// args: [2, 4, 6, 8]

说法定义纠正

  1. 箭头函数的 this 是绑定到父函数 foo 的,其实不是,只是沿用。因为箭头函数外部没有做任何的绑定操作。
  2. 局部变量 this 上面的例子是为了阐明局部变量 this
function foo() {
   var self = this;
   setTimeout(function() {console.log("id:" + this.id);
   },100);
}

foo.call({ id: 42} );
// id:undefined

这里为什么是 undefined,因为这里 setTimeout 是 windows 对象的属性。this 指向 window。

而后批改了一下:

function foo() {
   var self = this;
   setTimeout(function() {console.log("id:" + self.id);
   },100);
}

foo.call({ id: 42} );
// id:42

这里是因为 self 对于 function 的执行是一个执行上下文变量环境 outer 指向的蕴含以后函数的闭合函数变量环境。这里和 this 没有任何关系。

这里提一下:“因为 this 无论如何都是部分的”。this 都是函数执行的 this 绑定规定来决定的。

作为对象的一个办法

参照:上文 foo.bar 函数调用的解析

作为一个构造函数

[[construct]]

  1. 令 obj 为新创建的 ECMAScript 原生对象。
  2. 按照 8.12 设定 obj 的所有外部办法。
  3. 设定 obj 的 [[Class]] 外部办法为 “Object”。
  4. 设定 obj 的 [[Extensible]] 外部办法为 true。
  5. 令 proto 为以参数 “prototype” 调用 F 的 [[Get]] 外部属性的值。
  6. 如果 Type(proto) 是 Object,设定 obj 的 [[Prototype]] 外部属性为 proto。
  7. 如果 Type(proto) 不是 Object,设定 obj 的 [[Prototype]] 外部属性为 15.2.4 形容的规范内置的 Object 的 prototype 对象。
  8. 以 obj 为 this 值,调用 [[Construct]] 的参数列表为 args,调用 F 的 [[Call]] 外部属性,令 result 为调用后果。
  9. 如果 Type(result) 是 Object,则返回 result。
  10. 返回 obj

咱们能够看到 this 绑定在以后创立的对象。

作为一个 DOM 事件处理函数

this 指向触发事件的元素,也就是始事件处理程序所绑定到的 DOM 节点。

var ele = document.getElementById("id");
ele.addEventListener("click",function(e){console.log(this);
  console.log(this === e.target); // true
})

这里可能不太好晓得。从文献中理解到:

The event listener is appended to target’s event listener list

interface EventTarget {constructor();

  undefined addEventListener(DOMString type, EventListener? callback, optional (AddEventListenerOptions or boolean) options = {});
  undefined removeEventListener(DOMString type, EventListener? callback, optional (EventListenerOptions or boolean) options = {});
  boolean dispatchEvent(Event event);
};

callback interface EventListener {undefined handleEvent(Event event);
};

dictionary EventListenerOptions {boolean capture = false;};

dictionary AddEventListenerOptions : EventListenerOptions {
  boolean passive = false;
  boolean once = false;
};

依据 EventTarget 的属性,能够看到 callback 是一级属性,所以 Event.callback 的执行的 this 指向的是 EventTarget。当然具体的实现没有看到

从侧面理解:还有一种写法:button.onclick = foo。这种就能够很好的理解了。

this 优先级

当初咱们能够依据优先级来判断函数在某个调用地位利用的是哪条规定。能够依照上面的程序来进行判断:

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

论断:new 调用 > call、apply、bind 调用 > 对象上的函数调用 > 一般函数调用

// 例子起源:若风 https://juejin.cn/post/6844903746984476686#heading-12
var name = 'window';
var person = {name: 'person',}
var doSth = function(){console.log(this.name);
    return function(){console.log('return:', this.name);
    }
}
var Student = {
    name: 'rod',
    doSth: doSth,
}
// 一般函数调用
doSth(); // window
// 对象上的函数调用
Student.doSth(); // 'rod'
// call、apply 调用
Student.doSth.call(person); // 'person'
new Student.doSth.call(person); // Uncaught TypeError: Student.doSth.call is not a constructor

最初这一行说一下,因为. 运算符的优先级高于 new。所以这里是 Student.doSth.call 作为 new 的构造函数。然而因为 call 的办法执行的时候,执行的是 func 的 [[call]] 办法。想要吊用 new 的话须要调用[[Construct]] 属性

call 的调用

当以 thisArg 和可选的 arg1, arg2 等等作为参数在一个 func 对象上调用 call 办法,采纳如下步骤:

  1. 如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异样。
  2. 令 argList 为一个空列表。
  3. 如果调用这个办法的参数多余一个,则从 arg1 开始以从左到右的程序将每个参数插入为 argList 的最初一个元素。
  4. 提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 外部办法,返回后果。

正文完
 0