1. 函数声明
-
具名函数
function 函数名(形参){ 语句 }每个函数都有返回值,如果没有return显示指明,那么返回undefined
注意:将具名函数赋值给变量,用原来的函数名无法调用此函数,需要用新的函数名调用
let a = function fn(x,y){ return x+y } fn(1,2) //Uncaught ReferenceError: fn is not defined a(1,2) //3 -
匿名函数,没有函数名的函数
let fn = function(){}将一个匿名函数赋值给fn,也称函数表达式,没有函数提升
-
构造函数
let f = new Function('x','y','return x+y')
2. 函数的调用
定义一个函数并不会自动的执行它。定义了函数仅仅是赋予函数以名称并明确函数被调用时该做些什么。调用函数才会以给定的参数真正执行这些动作。
let a = 1
function fn(){
setTimeout(()=>{
console.log(a)
},0)
}
fn()
a = 2
// 2
函数的声明放在哪里与函数的执行无关
3. let 与 for循环
let i = 0
for( i = 0; i < 6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
// 输出6个'6'
setTimeOut需要浏览器提供的计时器线程来计时,属于异步任务。JavaScript属于单线程语言,将setTimeOut交给浏览器计时后,自己会转到执行主线程的代码,即上面例子中的for循环,当循环执行完成后,读取异步消息队列里的任务setTimeOut,由于全局定义了一个i,所以此时,i = 6,共有6个setTimeOut,输出6个6
for( let i = 0; i < 6; i++){
setTimeout(()=>{
console.log(i)
},0)
}
//输出 0,1,2,3,4,5
此时看上去也只是定义了一个i,为啥输出结果会不一样?
因为
JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量
i时,就在上一轮循环的基础上进行计算。
这个是JavaScript底层实现的,类似于
let i = 0
if( 0 < 6 ){
let k = i; // 底层实现
setTimeout(()=>{
console.log(k)
},0)
}
i++;
if( 1 < 6 ){
let k = i;
setTimeout(()=>{
console.log(k)
},0)
}
...
4. 函数作用域
函数作用域: 一个函数可以访问定义在其范围内的任何变量和函数
作用域链: 当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
**静态作用域(词法作用域):**函数定义时决定函数的作用域。(JS只有静态作用域)
let a = 'global'
function f1(){
console.log(a)
}
function f2(){
let a = 'local'
f1()
}
f2()
//global
执行f1时,根据作用域链的规则,会查找局部作用域中是否存在a → 没有 → 查找全局作用域的a →存在,并打印出
闭包: 如果一个函数用到了外部的变量,那么这个函数加变量就是闭包(出现块级作用域之前JavaScript中,只有函数有局部作用域,所以才造就了闭包)
闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在,闭包会保留外层函数的内部变量,内存消耗很大,解决办法可以在内部变量不使用时,把外部的引用置为 null。因此不能滥用闭包,否则会造成网页的性能问题
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第三者所引用,那么这两个互相引用的对象也会被回收。如果函数a被b引用,b又被a外的c引用,那函数a执行后不会被自动回收
3小节中的最后一段代码可以使用闭包输出0-5
for( var i = 0; i < 6; i++){
function fn(i){
setTimeout(()=>{
console.log(i)
},0)
}
fn(i)
}
下面例子,在严格模式下,this指向undefined,在非严格模式情况下,如果this为undefined,则默认为全局对象。在DOM中,全局对象就是window
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //The Window
this始终指向调用的对象,而例子中是一个匿名函数被调用,匿名函数的执行环境具有全局性,因此其 this 指向 window
5. 形参
arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。arguments是伪数组,形参就是对实参进行浅拷贝,其初始值为实参的值
let x = 1,y = 1;
let f = function(a, b) {
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(x, y) // 5
console.log(x) //1
严格模式下,修改arguments对象的值不会,影响真实的参数值
let x = 1,y = 1;
let f = function(a, b) {
'use strict'
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
console.log(f(x, y)) // 5
console.log(x) //1
6. 函数提升
采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。
f()
function f(){console.log(1)}
//1
但是,如果采用赋值语句定义函数,JavaScript 不会存在函数提升。
f()
let f = function(){console.log(1)}
//VM992:1 Uncaught ReferenceError: f is not defined
函数与变量同名,函数的声明会优先,即使function声明的函数放在后面,也会被JavaScript引擎提到当前作用域的顶部
var f = '2'
function f(){console.log(1)}
console.log(f) // 2
var f = function(){console.log(1)}
function f(){console.log(2)}
f(); // 1
7. 调用栈
JavaScript引擎在调用一个函数前,需要把函数所在的环境push到一个数组中,这个数组就叫调用栈,等函数执行完成,就会把当前环境pop出来,然后return到之前的环境,继续执行后续的代码
8. this
无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象。
在函数内部,this的值取决于函数被调用的方式。浏览器中如果未显示指定this,this默认为window,严格模式下,为undefined;
function f(){console.log(this)}
f(); // Window
function f(){
'use strict';
console.log(this)
}
f(); // undefined
8.1 call & apply
如果要想把 this 的值从一个环境传到另一个,就要用 call 或者apply 方法(call和apply原理一样,只不过apply第二个参数是数组类型),如下方的示例所示。
class Person{
constructor(name){
this.name = name
}
sayHi(){
console.log('My name is '+ this.name)
}
}
let p = new Person('Mike')
p.sayHi() // 等同于 Person.prototype.sayHi.call({name:'Mike'})
p.sayHi()是隐式的将p传给this, call显示的指定this的值
非严格模式下,通过call指定this值时,如果传入的不是对象,会默认封装成对象,严格模式下不会封装
forEach是遍历数组,通过call显示指定this的值,可以用来遍历伪数组
Array.prototype.forEach.call({0:'aa',1:'bb',2:'cc',length:'3'},(item)=>{console.log(item)});
// aa
// bb
// cc
8.2 bind
bind,固定this的值。ECMAScript 5 引入了 Function.prototype.bind()。调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到了bind的第一个参数,无论这个函数是如何被调用的。
let f2 = Array.prototype.forEach.bind({0:'aa',1:'bb',2:'cc',length:'3'});
f2((i)=>{console.log(i)})
9. 箭头函数
没有arguments和this(箭头函数里面的this就是外面的this,且通过call指定不了this的值,会被忽略)
10. 立即执行函数
立即执行函数 在匿名函数前加上() + - ! ~这些运算符,可以使得函数立即执行,不用等待被调推荐使用!
JavaScript引擎如果先遇到运算符,再遇到function关键字,会自动将后面的识别为函数表达式
!function(){
console.log('立即执行函数')
}()
注意:
-
在函数表达式后面加(),可以立即执行
let f = function(){console.log('立即执行')}() -
在匿名函数后面加(),不可以立即执行
function(){console.log('不会执行')}() //Uncaught SyntaxError: Function statements require a function nameJavaScript引擎在遇到function时,会默认把它当作是函数声明,所以必须要有函数名
-
在function 声明函数时加(),不可以立即执行
function f(){console.log('不会执行')}() //Uncaught SyntaxError: Unexpected token ')'函数声明时,会进行函数提升,当提升后只剩下(),而这个()与前面声明的函数没有什么关系,所以会报错
let i = 0
for(i = 0; i<6; i++){
!function(){
setTimeout(()=>{
console.log(i)
},0)
}(i)
}
立即执行函数和闭包的区别 立即执行函数和闭包没有关系,虽然两者会经常结合在一起使用,但两者有本质的不同
立即执行函数只是函数的一种调用方式,只是声明完之后立即执行,这类函数一般都只是调用一次(可用于单例对象上),调用完之后会立即销毁,不会占用内存
闭包则主要是让外部函数可以访问内部函数的作用域,也减少了全局变量的使用,保证了内部变量的安全,但因被引用的内部变量不能被销毁,增大了内存消耗,使用不当易造成内存泄露