this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API 设计得更加简洁(避免显式传递上下文对象)并且易于复用。
1、this 并不是指向函数本身。
2、this 在任何情况下都不指向函数的词法作用域。在JS 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过JS 代码访问,它存在于JS 引擎内部。
this 指针
全局作用域下的this
在 JavaScript 中,全局作用域指的是没有包含在任何函数中的代码块。如果在全局作用域中使用 this,它的指向是宿主环境根对象,浏览器中是 Window 对象(在 Node.js 中则是 Global 对象)。
对象方法中的this
对象的函数会进行this 自动绑定,这并不代表函数的内部函数也会自动绑定this(通过箭头函数可以解决this 指向问题)。
构造函数中的this
构造器函数,通过new 关键字调用。
function Person(name, age) {
this.name = name;
this.age = age;
}
let p1 = new Person('Tom', 25);
console.log(p1); // Person {name: "Tom", age: 25}
从执行上下文看this
当一个函数被调用时,会创建一个活动记录(执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中用到。
this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用,跟定义无关。
调用位置就在当前正在执行的函数的前一个调用中。
function baz() {
// 当前调用栈:baz
// 因此,当前调用位置是全局作用域
console.log('baz')
bar() // <-- bar 的调用位置
}
function bar() {
// 当前调用栈:baz -> bar
// 因此,当前调用位置在baz 中
console.log('bar')
foo() // <-- foo 的调用位置
}
function foo() {
// 当前调用栈:baz -> bar -> foo
// 因此,当前调用位置在bar 中
console.log('foo')
}
baz() // <-- baz 的调用位置
- 作为函数被直接调用时,在严格模式下,函数内的this 会被绑定到undefined 上,在非严格模式下则会被绑定到全局对象window/global 上。
- 作为对象的方法被调用时,函数体内的this 会被绑定到该对象上。
- 使用new 方法调用构造函数时,构造函数内的this 会被绑定到新创建的对象上。
- 通过call/apply/bind 方法显示调用函数时,函数体内的this 会被绑定到指定参数的对象上。
- 在箭头函数中,this 的指向是由外层(函数或全局)作用域来决定的。
const o1 = {
text: 'o1',
fn: function() {
return this.text
}
}
const o2 = {
text: 'o2',
fn: function() {
return o1.fn()
}
}
const o3 = {
text: 'o3',
fn: function() {
var fn = o1.fn
return fn()
}
}
console.log(o1.fn()) // o1
console.log(o2.fn()) // o1
console.log(o3.fn()) // undefined
// 对象的方法
// 第二个console 中的o2.fn() 最终调用的还是o1.fn(),因此运行结果仍然是o1
// 第三个console 中的o3.fn() 通过var fn = o1.fn 的赋值进行了“裸奔”调用,因此这里的this 指向window,运行结果是undefined
// 如果需要让console.log(o2.fn()) 语句输出o2,不使用bind、call、apply?
const o2 = {
text: 'o2',
fn: o1.fn // 提前进行赋值操作,将函数fn 挂载到o2 对象上,fn 最终作为o2 对象的方法被调用
}
console.log(o2.fn())
绑定规则
默认绑定 - window/global/undefined
// 非严格模式,由于函数调用时前面并未指定任何对象,this 指向全局对象window
function fn1() {
let fn2 = function () {
console.log(this) // window
fn3()
}
console.log(this) // window
fn2()
}
function fn3() {
console.log(this) // window
}
fn1()
// 在严格模式环境中,默认绑定的this 指向undefined
function fn() {
console.log(this) // window
console.log(this.name) // 'shaun'
}
function fn1() {
"use strict"
console.log(this) // undefined
console.log(this.name) // TypeError: Cannot read property 'name' of undefined
}
var name = 'shaun'
fn()
fn1()
对于默认绑定来说,决定this 绑定对象的并不是调用位置是否出于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到undefined,否则this 会被绑定到全局对象。
// 函数以及调用都暴露在严格模式中
"use strict"
var name = 'shaun'
function fn() {
console.log(this) // undefined
console.log(this.name) // 报错
}
fn()
// 如果在严格模式下调用不在严格模式中的函数,并不会影响this 指向
var name = 'shaun'
function fn() {
console.log(this) //window
console.log(this.name) //shaun
}
(function () {
"use strict"
fn()
}())
隐式绑定 - 对象属性
// 对象的属性
function fn() {
console.log(this.name)
}
let obj = {
name: 'shaun',
func: fn
}
obj.func() // shaun
// 如果函数调用前存在多个对象,this 指向距离调用自己最近的对象
function fn() {
console.log(this.name)
}
let obj = {
name: 'alfred',
func: fn,
}
let obj1 = {
name: 'shaun',
o: obj
}
obj1.o.func() // alfred
var length = 1
function fn() {
console.log(this, this.length)
}
var lists = [fn, 11, 22, 33, 44, 55]
lists[0]()
// [ƒ, 11, 22, 33, 44, 55] 6
隐式丢失 - 变量赋值(函数别名)
function foo() {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo // 函数别名!
var a = 'oops!'
bar() // 'oops!'
隐式丢失 - 传入回调函数
var name = '雨'
let obj = {
name: '风',
fn: function () {
console.log(this.name)
}
}
// 1 自定义
function fn1(param) {
param()
}
fn1(obj.fn) // '雨'
// 2 JS 内置函数
// 如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境,而不是定义时所在的那个对象。严格模式,会被设置为 undefined。
setTimeout(obj.fn, 100) // ‘雨'
显式绑定 - call/apply/bind
显式绑定是指通过call
、apply
方法改变this 的行为,相比于隐式绑定,能清楚地感知 this 指向变化过程。
如果传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this 的绑定对象,这个原始值就会被转换成它的对象形式(new String()、new Boolean()、new Number())。
硬绑定
function foo() {
console.log(this.a)
}
var obj = {
a: 2
}
var bar = function() {
foo.call(obj)
}
bar() // 2
setTimeout(bar, 100) // 2
// 硬绑定的bar 不可能再修改它的this
bar.call(window) // 2
// 创建函数bar(),并在它的内部手动调用foo.call(obj),因此强制把foo 的this 绑定到了obj。
// 无论之后如何调用函数bar,它总会手动在obj 上调用foo。
创建一个包裹函数,传入所有的参数并返回接收到的所有值:
function foo(something) {
console.log(this.a, something)
return this.a + something
}
var obj = {
a: 2
}
var bar = function() {
return foo.apply(obj, arguments)
}
var b = bar(3) // 2 3
console.log(b) // 5
被忽略的this
如果把null 或者undefined 作为this 的绑定对象传入call、apply 或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
let obj1 = {
name: 'obj1'
}
let obj2 = {
name: 'obj2'
}
var name = 'global'
function fn() {
console.log(this.name)
}
fn.call(undefined) // global
fn.apply(null) // global
fn.bind(undefined)() // global
什么情况会传入null?
function foo(a, b) {
console.log('a:' + a + ', b:' + b)
}
// 把数组展开成参数
foo.apply(null, [2, 3]) // a: 2, b: 3
// 使用bind(..) 进行柯里化
var bar = foo.bind(null, 2)
bar(3) // a: 2, b: 3
这两种方法都需要传入一个参数当作this 的绑定对象。如果函数并不关心this 的话,仍需要传入一个占位值,这是null 可能是一个不错的选择。
然而,总是使用null 来忽略this 绑定可能产生一些副作用。如果某个函数确实使用了this(如第三方库中的一个函数),那默认绑定规则会把this 绑定到全局对象,这将导致不可预计的后果。更安全的做法是传入一个特殊的对象。
// DMZ 空对象
var ø = Object.create(null)
foo.apply(ø, [2, 3])
call
在指定this 和arguments (参数)调用函数或方法的场景。
call() 方法创建并返回一个新函数,并绑定在传入的对象上。
- 第一个参数函数体内的this 指向,可不指定(在严格模式下是undefined,默认会绑定为全局对象)
- 第二个参数,接收任意个参数。
function sum(num1, num2) {
return num1 + num2
}
function callSum2(num1, num2) {
return sum.call(this, num1, num2)
}
// 参数对应
function func(a, b, c) {
console.log(a, b, c)
}
func.call(null, 1, 2, 3) // 1 2 3
func.call(null, [1, 2, 3]) // [1, 2, 3] undefined undefined
// 当调用 greet 方法的时候,该方法的this值会绑定到 obj 对象。
function greet() {
var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ')
console.log(reply)
}
var obj = {
animal: 'cats',
sleepDuration: '12 and 16 hours'
}
greet.call(obj) // cats typically sleep between 12 and 16 hours
使用场景
对象的继承
function superClass () {
this.a = 1
this.print = function () {
console.log(this.a)
}
}
function subClass () {
// 执行函数,this 继承了 superClass 的 print 方法和 a 变量
superClass.call(this)
this.print()
}
subClass() // 1
类(伪)数组使用数组方法
// slice() - 浅拷贝,返回一个新的数组对象
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"))
手写call
Function.prototype.call = function (context = window) {
if (typeof this !== 'function') {
return new TypeError('error')
}
context.fn = this
// 将context 后面的参数取出来
const args = [...arguments].slice(1)
const res = context.fn(...args)
delete context.fn
return res
}
/* 以下是对实现的分析:
- 首先 context 为可选参数,如果不传的话默认上下文为 window
- 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
- 因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
- 然后调用函数并将对象上的函数删除 */
Function.prototype.call2 = function(context, ...args) {
context = context || window;
context.__fn = this;
const res = context.__fn(...args);
delete context.__fn;
return res;
}
Function.prototype.myCall2 = function(context, ...params) {
context = context || window;
// Object 此时充当了一个构造函数的作用,可以理解为类型转换
!/^(object|function)&/.test( typeof context ) ?
context = Object(context) : null;
let _this = this, result = null, UNIQUE_KEY = Symbol('UNIQUE_KEY');
context[UNIQUE_KEY] = _this;
res = context[UNIQUE_KEY](...params);
delete context[UNIQUE_KEY];
return res;
}
// TEST CASE
const a = {
name: 'shaun'
};
function sayName() {
console.log(this.name);
}
sayName.myCall2(a);
apply
apply() 方法接收两个参数:
- 第一个参数函数体内的this 指向。如果不传,默认是全局对象window。
- 一个参数数组或类数组,可以是Array 的实例,也可以是arguments 对象
function sum(num1, num2) {
return num1 + num2
}
function callSum1(num1, num2) {
// this 值等于window,因为是在全局作用域中调用的
return sum.apply(this, arguments) // 传入arguments 对象
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2]) // 传入数组
}
console.log(callSum1(10, 10)) // 20
console.log(callSum2(10, 10)) // 20
function func(a, b, c) {
console.log(a, b, c)
}
func.apply(null, [1, 2, 3]) // 1 2 3
func.apply(null, {
0: 1,
1: 2,
2: 3,
length: 3
}) // 1 2 3
使用场景
获取数组中的最值
// 重要的不是 this 的绑定对象,而是 apply 将 array 的数组拆解了作为参数给 Math.max
let max = Math.max.apply(null, array)
let min = Math.min.apply(null, array)
数组合并
let arr1 = [1, 2, 3]
let arr2 = [4, 5, 6]
Array.prototype.push.apply(arr1, arr2)
// 这里相当于把 arr2 作为 apply 的第二个参数,把 arr2 拆解了 -- arr1.push(4,5,6)
// arr1 = [1, 2, 3, 4, 5, 6]
手写apply
/* 实现分析:
- 首先 context 为可选参数,如果不传的话默认上下文为 window
- 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
- 因为 apply 可以传入数组作为调用函数的参数,所以需要将参数剥离出来
- 然后调用函数并将对象上的函数删除 */
Function.prototype.apply2 = function(context, args) {
context = context || window;
context.__fn = this;
const res = context.__fn(...args);
delete context.__fn;
return res;
}
Function.prototype.applyFn = function (targetObject, argsArray) {
if (typeof argsArray === undefined || argsArray === null) {
argsArray = []
}
if (typeof targetObject === undefined || targetObject === null) {
targetObject = window
}
targetObject = new Object(targetObject)
// const targetFnKey = 'targetFnKey'
const targetFnKey = Symbol()
targetObject[targetFnKey] = this
const result = targetObject[targetFnKey](...argsArray)
delete targetObject[targetFnKey]
return result
}
// 函数体内的this 指向了调用applyFn 的函数。为了将该函数体内的this 绑定在targetObject 上,采用隐式绑定的方法:
// targetObject[targetFnKey](...argsArray)
// 这里存在一个问题:如果targetObject 对象本身就存在targetFnKey 这样的属性,那么在使用applyFn 函数时,原有的targetFnKey 属性值就会被覆盖,之后被删除。
// 解决方案时使用ES6 Symbol() 来保证键的唯一性,或者使用Math.random() 实现独一无二的键。
const targetFnKey = Symbol()
call vs apply
Function.prototype.apply 和Function.prototype.call 的作用是一样的,区别在于传入参数的不同:
- 第一个参数都是指定函数体内this 的指向
- 第二个参数开始不同,apply 是传入带下标的集合,数组或类数组;call 从第二个开始传入的参数是不固定的,都会传给函数作为参数
- call 比apply 的性能要好
bind
bind() 方法会创建一个新的函数,其this 值会被绑定到传给bind() 的对象。
window.color = 'red'
var o = {
color: 'blue'
}
function sayColor() {
console.log(this.color)
}
let objectSayColor = sayColor.bind(o)
objectSayColor() // blue
function func(a, b, c) {
console.log(a, b, c)
}
const func1 = func.bind(null, 'D')
func1('A', 'B', 'C') // D A B
func1('B', 'C') // D B C
// 如果连续 bind() 两次,亦或者是连续 bind() 三次
const bar = function(){
console.log(this.x)
}
const foo = {
x: 3
}
const sed = {
x: 4
}
const func = bar.bind(foo).bind(sed)
func() // ? => 3
const fiv = {
x: 5
}
const func = bar.bind(foo).bind(sed).bind(fiv)
func() // ? => 3
// 两次都仍将输出3,而非期待中的 4 和 5。多次 bind() 是无效的。
手写bind
// 简单版
Function.prototype.bind2 = function(context, ...args) {
context = context || window
let _this = this
return function(...args2) {
context.__fn = _this
const res = context.__fn(...[...args, ...args2])
delete context.__fn
return res
}
}
var value = 2;
var foo = {
value: 1
};
function bar(name, age) {
this.habit = 'reading';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.friend = 'lucius';
var bindFoo = bar.bind(foo, 'alfred');
var obj = new bindFoo(18);
console.log(obj.habit);
console.log(obj.friend);
// bind 的实现需要解决以下4个问题:
// 1、this 指向问题
// 2、传参处理
// 3、构造函数处理
// 4、原型的修改
Function.prototype.bind2 = function(context) {
if (typeof this !== 'function') {
throw new Error('this is not a function');
}
var _this = this;
// var args = Array.prototype.slice.call(arguments, 1)
// ES6
var args = [...arguments].slice(1);
var fBound = function() {
// var bindArgs = Array.prototype.slice.call(arguments)
// ES6
var bindArgs = [...arguments];
// this instanceof fBound 生成的结果是fBound 的实例,作为构造函数 fBound 等同于bindFoo
// this instanceof fBound:obj instanceof fBound 若为true,obj.value => undefined
return _this.apply(this instanceof fBound ? this : context, [
...args, ...bindArgs
])
// 或者写成 var finalArgs = args.concat(bindArgs)
// return _this.apply(this instanceof fBound ? this : context, finalArgs)
};
// fBound.prototype = this.prototype;
// 这种写法不太好,修改fBound 的原型会修改bar 的原型
// 可以定义一个空函数作为周转 => 采用寄生组合继承
var fNOP = function() {};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
call、apply与bind 区别
// 以下3段代码是等价的
const target = {}
fn.call(target, 'arg1', 'arg2')
const target = {}
fn.apply(target, ['arg1', 'arg2'])
const target = {}
fn.bind(target, 'arg1', 'arg2')()
- call、apply 与bind 都用于改变this 绑定,但call、apply 在改变this 指向的同时还会执行函数,而bind 在改变this 后是返回一个全新的绑定函数,这也是为什么bind 后还加了一对括号 () 的原因。
- bind 属于硬绑定,返回的 boundFunction 的 this 指向无法再次通过bind、apply或 call 修改;call与apply 的绑定只适用当前调用,调用完就没了,下次要用还得再次绑。
- call 与apply 功能完全相同,唯一不同的是call 方法传递函数调用形参是以散列形式,而apply方法的形参是一个数组。在传参的情况下,call 的性能要高于apply,因为apply 在执行时还要多一步解析数组。
// 修改 boundFunction 的 this 指向
let obj1 = {
name: '风'
}
let obj2 = {
name: '雨'
}
var name = '雷'
function fn() {
console.log(this.name)
}
fn.call(obj1) // 风
fn() // 雷
fn.apply(obj2) // 雨
fn() // 雷
let boundFn = fn.bind(obj1)
boundFn() // 风
boundFn.call(obj2) // 风
boundFn.apply(obj2) // 风
boundFn.bind(obj2)() // 风
let obj = {
name: '听风是风'
}
function fn(age, describe) {
console.log(`我是${this.name},我的年龄是${age},我非常${describe}!`)
}
fn.call(obj,'26','帅') // 我是听风是风,我的年龄是26,我非常帅
fn.apply(obj,['26','帅']) // 我是听风是风,我的年龄是26,我非常帅
new 绑定
使用new 操作符会执行以下步骤:
- 创建一个新对象
- 将新对象的原型设置为构造函数的prototype 属性(为新创建的对象添加属性
__proto__
,将该属性链接至构造函数的原型对象) - 将新对象作为this 对象调用构造函数
- 如果构造函数返回一个对象,则返回该对象;否则返回这个新对象
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name + ', and I am ' + this.age + ' years old.');
};
let person = new Person('John', 30);
// person 能够获取到构造函数中的this 指向的属性与原型上的方法
person.sayHello(); // 输出 "Hello, my name is John, and I am 30 years old."
// 1、创建一个新对象 person。
// 2、将 person 的原型设置为 Person.prototype。
// 3、将构造函数 Person 的 this 对象设置为 person,并执行构造函数内部的代码。
// 4、因为构造函数 Person 没有返回值,所以返回的是新创建的对象 person。
如果在构造函数中出现了显式return 的情况,可以细分为两种场景:
// 场景1-1
function Foo() {
this.uesr = 'Lucius'
const o = {}
return o
}
const instance = new Foo()
console.log(instance.user) // undefined,此时instance 返回的是空对象o
// 场景1-2
var puppet = {
rules: false
}
function Emperor() {
this.rules = true
return puppet
}
var emperor = new Emperor()
console.log(emperor) // { rules: false }
// puppet 对象最终作为构造函数调用的返回值,而且在构造函数中对函数上下文的操作都是无效的。
// 场景2
function Foo() {
this.user = 'Lucius'
return 1
}
const instance = new Foo()
console.log(instance.user) // Lucius,instance 此时返回的是目标对象实例this
【总结】
- 如果构造函数返回的是一个对象,则该对象将作为整个表达式的值返回,而传入构造函数的this 将被丢弃
- 如果返回的是非对象类型(如基本类型),则忽略返回值,返回新创建的对象(this 指向实例)
【实现new】
function Person() {};
var person = new Person();
var person = objectFactory(Person, name, age);
function objectFactory() {
var obj = new Object();
var Constructor = [].shift.call(arguments); // Constructor --> Person, arguments --> name, age
obj.__proto__ = Constructor.prototype; // obj --> Constructor
var ret = Constructor.apply(obj, arguments); // 绑定了this
return typeof ret === 'object' ? ret : obj
};
function myNew() {
const obj = {}
const con = [].shift.call(arguments)
obj.__proto__ = con.prototype
const res = con.apply(obj, arguments)
return res instanceof Object ? res : obj
}
/* 以下是对实现的分析:
- 创建一个空对象
- 获取构造函数
- 设置空对象的原型
- 绑定 this 并执行构造函数
- 确保返回值为对象 */
function myNew(constructor, ...args) {
let obj = Object.create(constructor.prototype);
let res = constructor.apply(obj, args);
return res instanceof Object ? res : obj;
}
Object.create() 方法是ES5 中新增的方法,用于创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。被创建的对象会继承另一个对象的原型,在创建新对象时还可以指定一些属性。
// 首先通过 Object.create() 方法创建了一个空对象 obj,并将其原型设置为构造函数的 prototype 属性。
// 然后使用 apply() 方法将构造函数的 this 指向该对象,并传递了参数 args。
// 最后判断构造函数的返回值是否为一个对象,如果是,则返回该对象,否则返回创建的新对象 obj。
// 需要注意的是,由于 new 操作符在使用时还会进行一些额外的处理,如设置该对象的 constructor 属性等,因此以上实现方式并不完整。
箭头函数的this
箭头函数的this 指向取决于外层作用域中的this,外层作用域或函数的this 指向谁,箭头函数中的this 便指向谁。
var obj = {
name: 'abc',
fn: () => {
console.log(this.name)
}
}
obj.name = 'bcd'
obj.fn()
// 这里函数执行的时候外层是全局作用域,所以this 指向window,window 对象下没有name 属性,所以是undefined。
function fn() {
return () => {
console.log(this.name)
}
}
let obj1 = {
name: '风'
}
let obj2 = {
name: '雨'
}
let bar = fn.call(obj1)
bar() // 风
bar.call(obj2) // 风
// 由于fn 中的this 绑定到了obj1 上,所以bar (引用箭头函数)中的this 也会绑定到obj1 上,箭头函数的绑定无法被修改。
fn.call(obj1)() // fn this 指向obj1,箭头函数this 也指向obj1
fn.call(obj2)() // fn this 指向obj2,箭头函数this 也指向obj2
// 箭头函数的this 取决于外层作用域的this,fn 函数执行时this 指向了obj1,所以箭头函数的this 也指向obj1
绑定优先级
如果一个函数调用存在多种绑定方法,this 绑定优先级为:
显式绑定 > 隐式绑定 > 默认绑定
new 绑定 > 隐式绑定 > 默认绑定
// e.g.1
let obj = {
name:'first',
fn: function () {
console.log(this.name)
}
}
obj1 = {
name:'second'
}
obj.fn.call(obj1) // second
// e.g.2
function foo(a) {
console.log(this.a)
}
const obj1 = {
a: 1,
foo: foo
}
const obj2 = {
a: 2,
foo: foo
}
obj1.foo.call(obj2) // 2
obj2.foo.call(obj1) // 1
// e.g.3
function foo(a) {
this.a = a
}
const obj1 = {}
var bar = foo.bind(obj1) // 将bar 函数中的this 绑定为obj1 对象
bar(2)
console.log(obj1.a) // 2
// e.g.4
obj = {
name: '雨',
fn: function () {
this.name = '风'
}
}
let echo = new obj.fn()
console.log(echo.name) // 风
面试
1、this 指向问题的理解
2、箭头函数的特性
3、手写实现call、apply、bind 函数
4、new 创建一个对象时,做了哪些事情?
// e.g. 1
var out = 25;
var inner = {
out: 20,
func: function () {
var out = 30;
return this.out;
}
};
console.log((inner.func, inner.func)());
console.log(inner.func());
console.log((inner.func)());
console.log((inner.func = inner.func)());
// 25、20、20、25
- 逗号操作符会返回表达式中的最后一个值,这里为inner.func 对应的函数,注意是函数本身,然后执行该函数,该函数并不是通过对象的方法调用,而是在全局环境下调用,所以this 指向window,打印出来的当然是window 下的out
- 这个显然是以对象的方法调用,那么this 指向该对象
- 加了个括号,看起来有点迷惑人,但实际上(inner.func)和inner.func 是完全相等的,所以还是作为对象的方法调用
- 赋值表达式和逗号表达式相似,都是返回的值本身,所以也相对于在全局环境下调用函数
// e.g. 2
function fn (){
console.log(this)
}
var arr = [fn]
arr[0]() // 打印出arr数组本身,[f]
函数作为某个对象的方法调用,this 指向该对象,数组显然也是对象,obj['fn']
。
扩展 - 软绑定
对指定的函数进行封装,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj 绑定到this,否则不会修改this。
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this
// 捕获所有curried 参数
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
}
}
function foo() {
console.log('name:' + this.name)
}
var obj = { name: 'obj' }
var obj2 = { name: 'obj2' }
var obj3 = { name: 'obj3' }
var fooOBJ = foo.softBind(obj)
fooOBJ() // name: obj
obj2.foo = foo.softBind(obj)
obj2.foo() // name: obj2
fooOBJ.call(obj3) // name: obj3
setTimeout(obj2.foo, 10) // name: obj
【参考资料】
《JS 忍者秘籍》第2版 章节4
《你不知道的JavaScript》上卷 第2部分 章节1、2