JavaScript系列 -- 普通函数、箭头函数、构造函数的区别及其对应 this 指向

493 阅读8分钟

最近学JavaScript的时候被普通函数、箭头函数、构造函数这三个函数搞晕了,这里写一下学习笔记记录一下,防止遗忘 [笑哭]

普通函数和构造函数的区别

1. 创建变量和方法的方式不同

  • 普通函数里创建变量和方法
function fn(){
    let name = "John"
    let age = 18
    let say = function(){
        console.log(name,age,this)
    }
}

或

function fn(){
    let name = "John"
    let age = 18
    let say = () => {
        console.log(name,age,this)
    }
}
  • 构造函数里创建变量和方法
function fn(name, age) {
    this.name = name;
    this.age = age;
    this.say = function () {
        console.log(this.name,this.age,this);
    }
}

2. 调用方式不同:

  • 普通函数的调用
function fn(){
    let name = "John"
    let age = 18
    let say = function(){
        console.log(name,age,this)
    }
    say()
}
fn() // "John"  18  windowfunction fn(){
    let name = "John"
    let age = 18
    let say = function(){
        console.log(name,age,this)
    }
    return say
}
fn()() // "John"  18  window
  • 构造函数里函数的调用(先new出一个实例对象,再用.访问其属性/方法)
function fn(name, age) {
    this.name = name;
    this.age = age;
    this.say = function (){
        console.log(this.name,this.age,this);
    }
}
var f = new fn("John",18)
f.say() // "John"  18   fn {name: "John", age: 18, say: ƒ}

3. 作用不同:

构造函数的作用是用来新建实例对象的(我的理解是构造函数偏向更像一个Object)

值得注意的是:普通函数也可以 new 出来一个实例对象,但是由于没有对实例对象创建属性/方法(this.xxx = ...),所以得到的实例对象是个空对象:

function fn(){
    let name = "John"
    let age = 18
    let say = function(){
        console.log(name,age,this)
    }
}
var f = new fn
console.log(f) // {}

4. this 指向

构造函数的 this 指向由这个例子可以清楚的看到就是指向 new 创建出来的实例对象 f

function fn(name, age) {
    this.name = name;
    this.age = age;
    this.say = function (){
        console.log(this.name,this.age,this);
    }
}
var f = new fn("John",18)
f.say() // fn {name: "John", age: 18, say: ƒ}

普通函数的 this 指向就需要看是谁调用该函数

var name = "Jack"
var obj = {
    name: "John",
    say: function(){
        console.log(this.name,this)
    }
}
obj.say() // "John"  obj {name: "John", say: ƒ}
var s = obj.say
s() // "Jack"  window

obj.say()s() 的区别在于前面谁在调用,obj.say()很明显是 obj 调用了 say() 方法,所以 this 指向 obj,而 s() 前面没有.默认就是 window 调用 say() 方法,所以 this 指向 window

普通函数和箭头函数的区别

1. 写法不同

var fn = function(){
    console.log("普通函数")
}
var fn = () =>{
    console.log("箭头函数")
}
fn() // 调用方式

2. 箭头函数是匿名函数,不能作为构造函数,不能使用new

var fn = () =>{
    console.log("箭头函数")
}
var f = new fn // 报错:Uncaught TypeError: fn is not a constructor

3. 箭头函数不绑定 arguments,取而代之用 rest 运算符 ... 解决

  • arguments 属性:一个类数组的Arguments对象,里面包含了传递进来的所有实参。
  • rest 运算符语法: ... args(args为随便起的变量名)
  • rest 运算符作用:把传递进来的实参信息,都以数组的形式保存到args变量中
var fn = function(){
    console.log(arguments)
}
fn(1,2,3,4,5) // Arguments(5) [1, 2, 3, 4, 5, callee: ƒ, Symbol(Symbol.iterator): ƒ]

var fn = () =>{
    console.log(arguments)
}
fn(1,2,3,4,5) // 报错:Uncaught ReferenceError: arguments is not defined

var fn = (...a) =>{
    console.log(a)
}
fn(1,2,3,4,5) // [1, 2, 3, 4, 5]

值得注意的是箭头函数里要是没有 rest 参数 ... ,则只能拿到传入参数的第一个

var fn = (a) =>{
    console.log(a)
}
fn(1,2,3,4,5) // 1

4. this 指向

普通函数和箭头函数的最大区别是 this指向 :普通函数有自己的this,箭头函数里面没有this

上面提到普通函数的 this 指向是看谁调用它,this 就指向谁。

由于箭头函数没有this,所以我们定个规则来判断箭头函数的this指向:

  • 如果箭头函数被普通函数包含(注意不是obj),则this指向跟着最近一层普通函数指向;
  • 如果没有被普通函数包含,则箭头函数的this指向window 这里箭头函数的外层是普通函数 fn(),fn() 函数里面的 this 执行 obj,所以箭头函数的 this 跟着指向 obj
var obj = {
    fn: function(){
        console.log(this) // obj {fn: f}
        setTimeout(()=>{
            console.log(this) // obj {fn: f}
        },0)
    }
}
obj.fn()

箭头函数外面没有一层普通函数,所以会顺着指向最外面的 window

var obj = {
    fn: ()=>{
        console.log(this) // window
        setTimeout(()=>{
            console.log(this) // window
        },0)
    }
}
obj.fn()

如果把箭头函数里面改成普通函数写法,会怎样?

var obj = {
    fn: function (){
        console.log(this) // obj {fn: f}
        setTimeout(function(){
            console.log(this) // window
        },0)
    }
}
obj.fn()

这里值得注意的是:如果定时器里面是普通函数的写法,则定时器里面的 this 默认指向 window;但如果定时器里面是箭头函数的写法,则定时器里面的 this 就被修改了,不是默认值了,具体值的判断就按照箭头函数的 this 的判断规则

5. 箭头函数不能用 call 等方法修改里面的 this,因为里面就没有 this

var obj = {
    fn: () => {
        console.log(this)
    }
}
obj.fn.call(obj) // window

再看一个例子:

let obj = {
    a: 10,
    b: function(n){
        let f = (n) => n + this.a
        return f(n)
    },
    c: function(n){
        let f = (n) => n + this.a
        let m = {
            a: 20
        }
        return f.call(m,n)
    }
}
console.log(obj.b(1)) // 11
console.log(obj.c(1)) // 11,而不是 21

f(n) 的结果是 11 可以理解,f.call(m,n)不是应该让 f 里的 this 指向对象 m 了吗,很明显,结果传入的参数只有 n,所以再次证明箭头函数不能用 call 等方法修改里面的 this 的结论

var a = 1
function F(){
    this.a = 2
    return ()=>{
        console.log(this.a)
    }
}
var f = new F()
f() (这里是调用了f实例里的箭头函数) // 2

归纳一下 this 指向的判断

  1. 普通函数的 this 指向是看谁调用它,this 就指向谁,跟在哪里调用没有关系
  2. 箭头函数没有this,判断规则是:如果箭头函数被普通函数包含,则this指向跟着最近一层普通函数指向;如果没有被普通函数包含,则箭头函数的 this 则指向 window
  3. 构造函数的 this 指向该构造函数创建出来的实例对象
  4. setTimeout里面如果是普通函数则 this 指向 window;如果里面是箭头函数,则 this 的规则按照箭头函数 this 指向判断规则

关于优先级及如何综合判断,参考vJS 中 this 指向问题 这篇文章,有个口诀可以看一下:

箭头函数、new、bind、apply 和 call、obj. 、直接调用、不在函数里

  • 看到箭头函数,其他都不用看了,按照箭头函数的 this 指向判断规则就行
  • new bind()返回的函数
function func() {
  console.log(this, this.__proto__ === func.prototype)
}

boundFunc = func.bind(1) // func() { console.log(this, this.__proto__ === func.prototype) }
var b = new boundFunc
console.log(b) // func {} true

利用 new 关键字对 bind() 方法返回的函数创建实例对象,可以看到 this 指向创建的实例对象了,而不是Number {1}

  • bind()返回的函数.apply()
function func() {
  console.log(this)
}

boundFunc = func.bind(1)
boundFunc.apply(2) // Number {1}

bind() 返回的函数使用 apply() 改变 this 指向,结果 this 指向仍然是 Number {1},而不是 Number {2}

改变 this 的指向

1. call() / apply() / bind() 方法

  • 函数.call(对象, 参数1, 参数2, 参数3,...)
  • 函数.apply(对象, 参数数组)
  • 函数.bind(对象, 参数1, 参数2, 参数3,...)

理解:fn.call/apply/bind(obj,...) 就是调用 fn 函数,fn 函数里面有 this 关键字的话,直接把 this 换成是 obj 即可

场景:

var p1 = {
    name : "小王",
    gender : "男",
    age : 24,
    say : function() {
        console.log(this.name + " , " + this.gender + " ,今年" + this.age); 
    }
}
var p2 = {
    name : "小红",
    gender : "女",
    age : 18
}
p1.say(); // "小王,男,今年24"

如何使用 p1 对象里的 say() 方法输出 p2 对象的数据呢?

p1.say.call(p2)
p1.say.apply(p2)
p1.say.bind(p2)()

这里说一下三种方法的区别:

1.1 bind() 区别于 call() 和 apply()

call 和 apply 都是对函数的直接调用,而 bind 方法返回的仍然是一个函数,因此后面还需要 () 来进行调用才可以。

p1.say.call(p2) // "小红 , 女 ,今年18"
p1.say.apply(p2) // "小红 , 女 ,今年18"
p1.say.bind(p2) // ƒ () { console.log(this.name + " , " + this.gender + " ,今年" + this.age);  }
p1.say.bind(p2)() // "小红 , 女 ,今年18"
1.2 apply() 区别于 call() 和 bind()——传参

如果指定函数的 this 指向后还要传入参数到函数里面,就直接在第一个参数后面加上去!!

apply() 传入参数相比于其他两种方法是:用一整个数组来代替一个一个的参数传入

function greet (person1, person2, person3) {
  console.log(`Hello, my name is ${this.name} and I know ${person1}, ${person2}, and ${person3}`)
}
const user = {
  name: 'Tyler',
  age: 27,
}
const people = ['John', 'Ruby', 'Deck']

greet.call(user,people[0],people[1],people[2])
// Hello, my name is Tyler and I know John, Ruby, and Deck

greet.apply(user,people)
// Hello, my name is Tyler and I know John, Ruby, and Deck

2. 把this用一个变量存起来

这里原本setTimeout里面的普通函数的this会指向window,而我们需要其指向F()

var num = 0;
function F (){
    var that = this;    //将this存为一个变量,此时的this指向f
    this.num = 1,
    this.getNum = function(){
        console.log(this.num);
    },
    this.getNumLater = function(){
        setTimeout(function(){
            console.log(that.num);    //利用闭包访问that,that是一个指向obj的指针
        }, 1000)
    }
}
var f = new F(); 
f.getNum(); // 1  打印的是obj.num,值为 1
f.getNumLater(); // 1  打印的是obj.num,值为 1

归纳一下函数的调用方式

1. 函数名方式(在函数里面调用)

function fn(){
    let say = function(){
        console.log(this)
    }
    say()
}
fn() // window

function fn() {
    var say = () => {
        console.log(this);
    }
    say()
}
fn(); // window

这种调用方式必须得有函数名称,其余三种可以没有函数名称,第二种可以有也可以没有

2. return 方式(闭包)(在函数外面调用)

function fn(){
    let say = function(){
        console.log(this)
    }
    return say
}
fn()() // window

function fn(){
    return function(){
        console.log(this)
    }
}
fn()() // window

function fn() {
    return () => {
        console.log(this);
    }
}
fn()(); // window

3. 自执行方式

(function(形参){
    console.log(形参)
})(实参)

((形参)=>{
    console.log(形参)
})(实参)

4. setTimeout()方式(有点像自执行)

setTimeout(function(){
    console.log(this)
},0)

setTimeout(()=>{
    console.log(this)
},0)

总结

  1. 构造函数是普通函数中的一种,特点是可以用来构造实例对象继承属性和方法
  2. this指向无非就三种:window、构造函数创建的实例对象、call/apply/bind方法里面第一个参数
  3. 普通函数的 this 指向由谁调用来判断,谁调用 this 就指向谁
  4. 构造函数的 this 指向由其创建出来的实例对象
  5. 箭头函数的 this 跟着最近一层的普通函数的this指向,要是没有被任何普通函数包裹,则会指向 window
  6. 改变 this 指向的有两种方案:call/apply/bind 和 var that = this

参考文章