最近学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 指向的判断
- 普通函数的 this 指向是看谁调用它,this 就指向谁,跟在哪里调用没有关系。
- 箭头函数没有this,判断规则是:如果箭头函数被普通函数包含,则this指向跟着
最近一层
普通函数指向;如果没有被普通函数包含,则箭头函数的 this 则指向window
- 构造函数的 this 指向该构造函数创建出来的
实例对象
- 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)
总结
- 构造函数是普通函数中的一种,特点是可以用来构造实例对象继承属性和方法
- this指向无非就三种:window、构造函数创建的实例对象、call/apply/bind方法里面第一个参数
- 普通函数的 this 指向由谁调用来判断,谁调用 this 就指向谁
- 构造函数的 this 指向由其创建出来的实例对象
- 箭头函数的 this 跟着最近一层的普通函数的this指向,要是没有被任何普通函数包裹,则会指向 window
- 改变 this 指向的有两种方案:call/apply/bind 和 var that = this