1.this的奥秘
不同于其它的编程语言,在JavaScript中,this表示函数当前执行的上下文,根据不同的调用方式决定了函数当前的执行上下文:
-
函数调用:fn( arguments )
-
方法调用:obj.fn( arguments )
-
构造函数:new Fn( arguments )
-
隐式调用: fn.call( obj, arguments )
每种调用模式都是以自己的方式定义上下文,另外严格模式下也会影响执行上下文,容易产生混淆. 不同的函数调用方式是如何影响this的呢?
要搞懂这个问题先了解一下几个术语的意思--- 以toString(123)为例:
- 函数调用:执行构成函数主体的代码,例如toString函数的调用主体是toString(123)
- 调用的上下文:指this在函数体内的值,例如arr.push('abc')的调用上下文就是arr
- 函数的作用域:指的是函数体内可以访问的变量,对象,函数的集合
2.函数的调用
函数的调用表达式为fn(arguments),函数的调用与方法的调用obj.fn()要区分开:
function person(name,age){
return `我叫${name},今年${age}岁了`
}
//函数调用,person等价于一个函数表达式,后面接(参数体)
person(薇恩,18)
//除此之外还有立即执行的函数表达式(function(...){...})
//例子中第一个圆括号内是一个表达式,计算结果是一个函数,第二个圆括号是对函数的调用
const vn = (function person(name,age){
return `我叫${name},今年${age}岁了`
})('薇恩',18)
2-1函数调用中的this
this在函数调用中是一个全局对象,它由执行上下文确定,在浏览器中全局对象是window
function sum(a,b){
console.log(this ===window)
this.love = 520
return a+b
}
sum() // true
window.love = 520
在浏览器中执行JS调用sum()时,自动将函数的this对象指向winow,也就是浏览器中的全局对象
当函数在任何函数体之外(顶层作用域)使用时,this指向window对象
console.log(this === window) //true
this.love = 520
console.log(window.love) // 520
2-2严格模式下函数调用的this
开头提到了严格模式下的this对象,那么严格模式下函数调用的this对象是指向哪里的呢?
** 严格模式下函数调用的this是指向undefined **
严格模式是在ECMAScript 5.1引入的,它提供了更好的安全性和更好的错误检查,启用严格模式在函数头部写入use strict;启用后严格模式会影响执行上下文,this在常规函数调用中指向undefined.与上述相反,执行上下文不再是全局对象
function fn(){
'use strict' //fn一下的子作用域(内部声明的所有函数)
console.log(this)
}
fn() //undefined
单个Javascript脚本可能包含严格模式和非严格模式
function fn1(){
console.log(this)
}
fn1() //window
function fn2(){
'use strict'
console.log(this)
}
fn2() //undefined
2-3 敲黑板:this在内部函数的时候
函数调用有一个需要注意的问题,通常认为this在内部函数中的情况与外部函数中的情况相同,确切的说,内部函数的上下文只依赖它的调用类型,不依赖于外部函数的上下文. 这个时候就可以通过call() / apply()来修改内部函数的上下文,亦或使用bind()创建绑定函数.
const obj = {
x:500,
y:20,
sum:function(){
console.log(this) // obj
function cla(){
console.log(this === obj) //false
return this.x + this.y
}
return cla()
}
}
obj.sum() // NaN
上面的例子中sum()是obj对象上面的方法,所以sum的this对象指向obj对象,cla方法定义在sum方法内部,在执行cla()的时候cla是作为一个函数调用,由它所在的执行环境来确定:此时的cla的执行上下文是window,外层函数sum的this对象对它没有影响,因此得不到预期的结果 为了是cla与sum的执行上下文一致,我们可以人为的修改this指向:
return cla.call(this)
除此之外我们还可以将外层函数的this对象保存在一个变量中,供cla函数执行的时候使用
sum:function(){
console.log(this) // obj
const that = this
function cla(){
console.log(that === obj) //true
return that.x + that.y
}
return cla()
}
还可以通过箭头函数的方法来定义cla 函数:
sum:function(){
console.log(this) // obj
cla = () => {
console.log(this === obj) //true
return this.x + this.y
}
return cla()
}
3.方法的调用
** 方法是存储在对象属性中的函数 **
const obj = {
method:function(){
return '阿里哈瑟哟'
}
}
const meg = obj.method()
上面是以属性访问的方式来来执行method方法,区分函数调用和方法调用是非常重要的,因为他们是不同的类型,正确的区分两者之间的区别有助于正确识别上下文
3-1 方法调用的this指向
**方法调用中,this指向这个方法的拥有者**
const obj = {
x:520,
sum:function(){
this.x += 1
}
}
obj.sum() //sum为obj对象的一个属性,以方法调用的方式来调用sum,this指向obj
另外一种情况,对象从原型上继承一个方法,当在对象上调用继承来的方法是,上下文依然指向调用对象本身:
const person = {
sayHi:function(){
console.log(this)
return this.name
}
}
const momo = Object.create(person)
momo.name = '欧巴'
momo.sayHi()
// Object.create()创建一个新对象,根据第一个参数设置其原型,momo对象继承person,执行momo.sayHi()的时候,momo是执行上下文
3-2 从对象中提取方法
对象中的方法可以提取到一个单独的变量中:const self = obj.method 当方法单独调用时(在没有对象的情况下调用),函数调用就会发生,此时的this指向全局对象window,严格模式下指向undefined
function Person(name,age){
this.name = name
this.age = age
this.sayHi = function(){
console.log(this)
console.log(`姓名:${this.name}---年龄:${this.age}`)
}
}
const momo = new Person('mingming',20)
setTimeout(momo.sayHi,1000)
你可能会认为输出结果是打印出momo的相关信息,但是结果并非如此
以上调用方法跟 :
const self = momo.sayHi
self()
是等价的(相当把momo.sayHi赋值给了setTimeout的第一个参数)
此时可以借用bind()来得到正确的结果:
setTimeout(momo.sayHi.bind(momo),1000)
4. 构造函数的调用
function Person(name,age){
this.name = name ? name : 'caicai'
this.age = age
}
Person.prototype.sayHi = function(){
console.log(`姓名:${this.name}---年龄${this.age}`)
}
const momo = new Person('xiaoming',18)
const roro = new Person
momo.sayHi()
ES6规定JS允许用class关键词来定义构造函数:
class Person{
constructor(name,action){
this.name = name
this.action =false
}
change(){
this.action = true
}
}
const momo = new Peroson('shuy',false)
momo.change()
构造函数Person创建了一个新的对象,它从原型继承了属性.构造函数的作用就是初始化实例,new调用构造函数,this指向实例对象 需要注意的是当执行 new obj.method()时,JS会执行构造函数调用而不是原来的方法调用:
new obj.method()相当于 const self = obj.method ---> new self
4-1构造函数中的this
构造函数中的this指向新创建的对象,它利用构造函数的参数初始化新的对象,设定属性的初始值,添加事件处理函数
JavaScript function Fn(){ console.log(this instanceof Fn) //true } const Fninstance = new Fn()
4-2在不使用new关键字的情况下调用构造函数
new关键字能保证构造函数的this对象指向实例,以下属于函数调用,上下文指向window对象
function Person(name,age){
this.name = name
this.age = age
}
const momo = Person('caicai',18)
momo.name //caicai
momo.age //18
console.log(momo === window) //true
5 隐式调用
call()或apply()属于隐式调用,第一个参数是函数执行时的上下文,当应该使用特定上下文执行函数时,隐式调用是非常有用的,隐式调用时可以用于模拟在一个对象上调用某个方法
function Runner(name) {
console.log(this instanceof Rabbit); // => true
this.name = name;
}
function Rabbit(name, countLegs) {
console.log(this instanceof Rabbit); // => true
Runner.call(this, name); //隐式调用了Runner
this.countLegs = countLegs;
}
const myRabbit = new Rabbit('White Rabbit', 4);
myRabbit; // { name: 'White Rabbit', countLegs: 4 }
6绑定函数
绑定函数是与对象链接的函数.通常使用bind()方法从原函数创建.原函数和绑定函数拥有相同的代码和作用域,但是执行时上下文不同.
方法 myFunc.bind(thisArg[, arg1[, arg2[, ...]]])接受第一个参数thisArg作为绑定函数执行时的上下文,并且它接受一组可选的参数 arg1, arg2, ...作为被调用函数的参数。它返回一个绑定了thisArg的新函数
function multiply(number) {
'use strict';
return this * number;
}
const double = multiply.bind(2);
double(3); // => 6
double(10); // => 20
6-1绑定函数中的this
**调用绑定函数的时候,this是bind()的第一个参数**
bind()的作用是创建一个新函数,调用该函数的时候,将上下文作为参数传递给bind的第一个参数.它可以是我们创建一个定义了this值得函数
const numbers = {
array: [3, 5, 10],
getNumbers: function() {
return this.array;
}
};
const boundGetNumbers = numbers.getNumbers.bind(numbers); //绑定了函数的执行上下文
boundGetNumbers(); // => [3, 5, 10]
// Extract method from object
const simpleGetNumbers = numbers.getNumbers;
simpleGetNumbers(); // => undefined (严格模式下报错)
6-2紧密的上下文绑定
bind(()创建一个永久的执行上下文,并始终保持它.一个绑定函数不能通过.call()或者.apply()来改变它的上下文,甚至是再次绑定也不会有什么作用.只有绑定函数的构造函数调用才能更改已经绑定的上下文
function getThis() {
'use strict';
return this;
}
const one = getThis.bind(1);
// 绑定函数调用
one(); // => 1
// 使用带有.apply()和.call()的绑定函数
one.call(2); // => 1
one.apply(2); // => 1
// 再次绑定
one.bind(2)(); // => 1
// 以构造函数的形式调用绑定函数
new one(); // => Object
只有new one()改变了绑定函数的上下文,其他方式的调用中this总是等于1。
7.箭头函数
箭头函数的语法简单,没有function关键字.当返回值只有一条语句时,甚至可以省略return关键字.箭头函数是匿名的,所以name属性是一个空字符串.这样它就没有词法上的函数名了.跟常规函数相反,箭头函数不提供arguments对象.但是在ES6中通过rest parameters修复了
**箭头函数的this指向定义它的上下文**
箭头函数不会创建自己的执行上下文,而是从定义它的外部函数中获取this,也就是说箭头函数在定义的时候已经绑定this了
class Person{
constructor(name,age){
this.name = name
this.age = age
}
sayHi(){
console.log(this) //Person
setTimeout(()=>{
console.log(this) //Person
},1000)
setTimeout(function(){
console.log(this) //window
},1000)
}
}
const momo = new Person('caicai',50)
momo.sayHi()
setTimeout使用与sayHi方法相同的上下文(momo对象),箭头函数从定义它的函数继承上下文
如果箭头函数在最顶层的作用域定义,则始终是全局对象window
const person = () => {
console.log(this) //window
}
箭头函数的this指向不能被修改:
const numbers = [1, 2];
(function() {
const get = () => {
console.log(this === numbers); // => true
return this;
};
console.log(this === numbers); // => true
get(); // => [1, 2]
// Use arrow function with .apply() and .call()
get.call([0]); // => [1, 2]
get.apply([0]); // => [1, 2]
// Bind
get.bind([0])(); // => [1, 2]
}).call(numbers);
无论如何调用箭头函数,他总是保留词汇上下文.箭头函数不能作构造函数
最好不用箭头函数定义对象上面的方法,往往得不到与你期待的效果
function Person(name,age){
this.name = name
this.age = age
}
Person.prototype.sayHi = () => {
console.log(this) //window
return `我叫${this.name},今年${this.age}`
}
const momo = new Person('caicai',50)
momo.sayHi() // 由于是箭头函数,this在函数定义的时候就已经绑定了
合理的写法是:
Person.prototype.sayHi = function(){
console.log(this) //window
return `我叫${this.name},今年${this.age}`
}