首页/文章/javascript

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

2023-11-23
281310 分钟
...

最近学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  window



function 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

参考文章

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2026-02-24
navigator.sendBeacon全指南
在前端开发中,埋点系统是必不可少的一环。我们经常需要在用户 关闭页面 、 刷新 或 跳转路由 时,向服务器发送最后一条统计数据(比如用户停留时长、页面跳出率)。 但这看似简单的需求,在实现时却危机四伏:请求发不出去?页面跳转卡顿?今天我们就来聊聊这个问题的终极解决方案 —— 。 一、 痛点与传统方案的挣扎 场景还原 当用户点击关闭按钮时,浏览器会触发生命周期事件( 或 )。如果我们直接使用普通的异步 AJAX ( 或 ) 发送请求,浏览...
学习
javascript2025-11-02
理解浏览器事件系统,从用户点击到事件对象的完整旅程
深入理解浏览器事件系统:从用户点击到事件对象的完整旅程 “当我点击页面按钮时,背后发生了什么?为什么回调函数能收到一个包含丰富信息的event对象?今天,让我们一起揭开浏览器事件系统的神秘面纱。” 一个令人困惑的现象 作为前端开发者,我们每天都在写这样的代码: 这段代码如此熟悉,以至于我们很少停下来思考:​ ​这个 对象到底从哪里来?它为什么能知道点击的精确坐标?为什么能识别是哪个元素被点击了?​ ​ 更神奇的是,当我们手动创建事件时:...
学习
javascript2025-10-01
实现大文件上传全流程详解
在日常开发中,大文件上传是个绕不开的坎——动辄几百 MB 甚至 GB 级的文件,直接上传不仅容易超时,还会让用户体验大打折扣。最近我用 Vue+Express 实现了一套完整的大文件上传方案,支持分片上传、断点续传、秒传和手动中。 一、先看效果:我们要实现什么? 先上核心功能清单,确保大家明确目标,知道我们要解决哪些实际问题: 大文件分片上传 :将文件切成固定大小的小片段分批上传,避免单次请求超时 秒传 :服务器已存在完整文件时,直接返...
学习
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-09-15
一张 8K 海报差点把首屏拖垮
你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。 老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。 解决方案:三条技术路线,你全踩了一遍 1\. 最偷懒:原生 一行代码就能跑,浏览器帮你搞定。 🔍 关键决策点 2020 年后现代浏览器全覆盖,IE 全军覆没。 必须写死 ,否则 CLS 会抖成 PPT。 适用场景...
学习
javascript2025-09-10
🚀 Web Worker让你的应用丝滑
🌟 引言 在日常的前端开发中,你是否遇到过这样的困扰: 大数据处理时页面卡死 :处理几万条数据时,页面直接卡成PPT,用户点击毫无反应 复杂计算阻塞UI :图片处理、数据分析等计算密集型任务让整个应用假死 文件上传/下载卡顿 :大文件操作时,其他功能完全无法使用 实时数据处理性能差 :WebSocket接收大量数据时,页面渲染严重滞后 今天分享6个Web Worker的核心技巧,让你的应用告别卡顿,用户体验丝滑如德芙! 💡 核心技巧...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录