函数:内部特殊值和函数方法
- 罗列 this 值的常见情况。
this 的值取决于多种因素。
(1) 全局上下文
this 在全局上下文中,即在任何函数体的外部时,都指向全局对象,在浏览器宿主环境中就是 window。
来看下面的例子:
let o1 = { this: this, o2: { this: [this], }, }; console.log(o1.this === window); // true console.log(o1.o2.this[0] === window); // true 复制代码
在这个例子中,对象 o1 的 this 属性引用 this 值,而这个 this 值在全局上下文中指向 window。同样的,o2 的 this 值引用一个数组对象,这个数组的第一个元素为 this,但无论如何 this 值都在全局上下文中,所以都指向 window。
(2) 标准函数上下文
this 出现在标准函数上下文中,此时的 this 取决于函数调用的方式。
- 在非严格模式下直接调用标准函数,则其内部的 this 指向全局对象。
来看下面几个例子:
function getThis() { return this; } console.log(getThis() === window); // -> true 复制代码
在这个例子中,getThis 是一个函数声明,它返回内部的 this 值。之后在全局上下文中直接调用了 getThis,它返回了全局对象。
非严格模式下直接调用标准函数,其内部的 this 值指向全局对象,而不论调用的上下文:
function foo() { function getThis() { console.log(this === window); // -> true } getThis(); } foo(); 复制代码
在这个例子中,getThis 在函数 foo 的上下文中直接调用,但不论是在全局上下文还是函数上下文中,非严格模式下直接调用标准函数,其内部的 this 都指向全局对象。
- 严格模式下直接调用标准函数,则其内部的 this 为 undefined。
来看下面几个例子:
function getThisInStrict() { 'use strict'; return this; } console.log(getThisInStrict() === window); // -> false console.log(getThisInStrict() === undefined); // -> true 复制代码
在这个例子中,函数 getThisInStrict 的作用域内使用了严格模式。之后,在全局上下文中调用了 getThisInStrict,它内部的 this 为 undefined 而不是全局对象。
同样的,严格模式下直接调用标准函数,其内部的 this 为 undefined,而不论调用的上下文。
- 标准函数作为方法调用时,this 指向直接调用者,而非间接调用者。
来看下面几个例子:
function getThis() { return this; } let o = { name: 'o', }; o.getThis = getThis; console.log(o.getThis().name); // -> 'o' 复制代码
在这个例子中,getThis 作为 o 的方法在全局上下文中调用。getThis 内部的 this 指向 o。
这个规则不受严格模式和调用方法时的上下文影响:
'use strict'; function getThis() { return this; } function foo() { const o = { name: 'o', }; o.getThis = getThis; console.log(o.getThis().name); // -> 'o' } foo(); 复制代码
在这个例子中,全局使用了严格模式。之后在函数 foo 上下文中,getThis 作为 o 的方法被调用。其内部的 this 指向 o。
复杂的情况是通过间接方式调用函数:
function getThis() { return this; } console.log(getThis.prototype.constructor === getThis); // -> true console.log(getThis.prototype.constructor() === getThis.prototype); // -> true let constructor = getThis.prototype.constructor; console.log(constructor() === window); // -> true 复制代码
在这个例子中,getThis.prototype.constructor 返回 getThis 函数对象本身。在执行 getThis.prototype.constructor()时,getThis 的直接调用者为 getThis.prototype,因此 this 指向 getThis.prototype。之后,将 getThis.prototype.constructor 赋给 constructor,并直接调用,因此 this 指向全局对象。
- 标准函数作为构造函数调用,this 指向正在构建的对象:
function Person() { this.name = 'Nicholas'; console.log(this instanceof Person); // -> true } new Person(); 复制代码
在这个例子中,Person 作为构造方法调用,this 指向被构建的对象,因此 this instanceOf Person
为 true。
(3) 箭头函数
箭头函数的 this 行为和标准函数的截然不同。
- 箭头函数在全局上下文中,则其 this 绑定全局对象,且严格模式和方法调用对该 this 没有影响。
来看下面几个例子:
const getThisInArrowFunc = () => { 'use strict'; return this; }; console.log(getThisInArrowFunc() === window); // -> true 复制代码
在这个例子中,getThisInArrowFunc 位于全局上下文,其内部使用了严格模式,但这不影响 this 的值。this 依旧绑定全局对象。
var name = 'window'; const o1 = { name: 'o1', o2: { name: 'o2', getThis: () => this, }, }; console.log(o1.o2.getThis().name); // -> 'window' 复制代码
在这个例子中,getThis 作为 o2 的方法被 o1 间接在全局上下文中调用,但不论是否作为方法被调用,getThis 都在全局上下文中,因此其内部的 this 还是绑定全局对象。
- 箭头函数在函数上下文中,则其 this 绑定紧邻的外层函数的 this。
var name = 'window'; function foo() { const o1 = { name: 'o1', o2: { name: 'o2', getThis: () => this, }, }; console.log(o1.o2.getThis().name); // window } foo(); 复制代码
在这个例子中,箭头函数 getThis 在 foo 函数的上下文中。这里 foo 在非严格模式下直接调用,所以函数 foo 的 this 指向全局对象,箭头函数绑定了该值。
当然,如过外层的函数 foo 以方法调用,则该箭头函数内部的 this 就绑定 foo 的直接调用者。
- new.target
ECMAScript 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。ECMAScript 6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。如果函数是正常调用的,则 new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数。
来看这个例子:
function foo() { const funcName = arguments.callee.name; if (new.target) { throw `函数${funcName}不能用作构造函数!`; } else { console.log(`函数${funcName}用为非构造函数!`); } } new foo(); foo(); 复制代码
在这个例子中,函数 foo 的名字使用 arguments.callee.name
取得,如果 foo 用作构造函数,则 new.target 指向构造函数本身,为非空对象,则抛出错误。如果 foo 没有用作构造函数,则 new.target 为 undefined,正常执行。
- arguments 对象
arguments 是一个类数组对象。
- 实现 Function.prototype.call
const { log } = console; Function.prototype.myCall = function (thisArg, ...args) { // 检查调用方法是不是function if(typeof this !== 'function') { throw new TypeError('Error') } // 默认window thisArg = thisArg || window // 添加方法 thisArg.fn = this const result = thisArg.fn(...args) // 删除方法 delete thisArg.fn; // 返回结果 return result }; 复制代码
- 学习 apply, bind 方法
apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或类数组对象)的形式提供的参数。
const numbers = [5, 6, 2, 3, 7]; const max = Math.max.apply(null, numbers); console.log(max); // expected output: 7 复制代码
- 用 apply 将数组各项添加到另一个数组
和concat不一样。concat是返回的新数组。
- 使用 apply 和内置函数
对于一些需要写循环以遍历数组各项的需求,我们可以用 apply 完成以避免循环。
注意:如果按上面方式调用apply,有超出 JavaScript 引擎参数长度上限的风险。
解决方法是切块数组,重复比较
function minOfArray(arr) { var min = Infinity; var QUANTUM = 32768; for (var i = 0, len = arr.length; i < len; i += QUANTUM) { var submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len))); min = Math.min(submin, min); } return min; } var min = minOfArray([5, 6, 2, 3, 7]); 复制代码
- 使用 apply 来链接构造器
你可以使用 apply 来链接一个对象构造器 (en-US),类似于 Java。在接下来的例子中我们会创建一个全局Function 对象的 construct 方法 ,来使你能够在构造器中使用一个类数组对象而非参数列表。
function MyConstructor () { for (var nProp = 0; nProp < arguments.length; nProp++) { console.log(arguments,this) this["property" + nProp] = arguments[nProp]; } } var myArray = [4, "Hello world!", false]; var myInstance = new MyConstructor(myArray); console.log(myInstance.property1); // logs "undefined" console.log(myInstance instanceof MyConstructor); // logs "true" console.log(myInstance.constructor); // logs "MyConstructor" 复制代码
function MyConstructor () { for (var nProp = 0; nProp < arguments.length; nProp++) { console.log(arguments,this) this["property" + nProp] = arguments[nProp]; } } var myArray = [4, "Hello world!", false]; var myInstance = new MyConstructor(...myArray); console.log(myInstance.property1); // logs "Hello world!" console.log(myInstance instanceof MyConstructor); // logs "true" console.log(myInstance.constructor); // logs "MyConstructor" 复制代码
Function.prototype.construct = function(aArgs) { var fConstructor = this, fNewConstr = function() { fConstructor.apply(this, aArgs); }; fNewConstr.prototype = fConstructor.prototype; return new fNewConstr(); }; function MyConstructor () { for (var nProp = 0; nProp < arguments.length; nProp++) { console.log(arguments,this) this["property" + nProp] = arguments[nProp]; } } var myArray = [4, "Hello world!", false]; var myInstance = MyConstructor.construct(myArray); console.log(myInstance.property1); // logs "Hello world!" console.log(myInstance instanceof MyConstructor); // logs "true" console.log(myInstance.constructor); // logs "MyConstructor" 复制代码
- Function.prototype.bind()
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
const module = { x: 42, getX: function() { return this.x; } }; window.x = 111 const unboundGetX = module.getX; console.log(unboundGetX()); // The function gets invoked at the global scope // expected output: 111 const boundGetX = unboundGetX.bind(module); console.log(boundGetX()); // expected output: 42 复制代码
偏函数
bind() 的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为 bind() 的参数写在 this 后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。
配合 setTimeout
在默认情况下,使用 window.setTimeout() 时,this 关键字会指向 window(或 global)对象。当类的方法中需要 this 指向类的实例时,你可能需要显式地把 this 绑定到回调函数,就不会丢失该实例的引用。
function LateBloomer() { this.petalCount = Math.ceil(Math.random() * 12) + 1; } // 在 1 秒钟后声明 bloom LateBloomer.prototype.bloom = function() { window.setTimeout(this.declare.bind(this), 1000); }; LateBloomer.prototype.declare = function() { console.log('I am a beautiful flower with ' + this.petalCount + ' petals!'); }; var flower = new LateBloomer(); flower.bloom(); // 一秒钟后,调用 'declare' 方法 复制代码
作为构造函数使用的绑定函数
- 实现 bind 方法
'use strict'; const { log } = console; Function.prototype.myBind = function myBind(thisArg, ...presetArgs) { // self指向绑定函数 var self = this; // 绑定函数先传的参数 var argsOne = Array.prototype.slice.call(presetArgs) var fbound = function() { // 返回的函数之后传入的参数 var argsTwo = Array.prototype.slice.call(arguments) // this fbound作为构造函数时 指向实例,直接调用指向window 那么就改为指向thisArg self.apply(this instanceof self ? this : thisArg , argsOne.concat(argsTwo)) } // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承函数的原型中的值 fbound.prototype = this.prototype; return fbound; }; function logInfo(a, b) { log(a, b); log(this); } const bindedLogInfo = logInfo.myBind('thisArg', 1, 2); bindedLogInfo(3, 4); // -> 1 2 // -> 'thisArg'