01、JS函数基础
函数定义
函数(方法)就是一段定义好的逻辑代码,函数本身也是一个object引用对象。三种构造方式:
🔸①函数申明:**function 函数名(参数){代码}**
,申明函数有函数名提升的效果,先调用,后申明(和var申明提升类似,比var提升更靠前)。
🔸②函数表达式:**var func=function(参数){代码}**
,定义变量指向函数,函数不需要命名。不过也可以像申明函数一样指定函数名,在其内部调用自己。
🔸③Function构造函数:new Function(参数,代码)
,支持多个构造参数,前面为函数参数,最后一个为函数体,使用不多。
function func1(a) {
console.log(a);
}
var func2 = function(b){
console.log(b);
}
var func3 = new Function('c','console.log(c)');
//调用函数
func1(1);
func2(2);
func3(3);
:::warning ❗注意:JS中没有方法重载,就是不允许相同的函数名,重名会被覆盖。 ::: 🔸return 返回值:
- 通过return 返回值,并结束方法,单return结束方法。
-
arguments参数
参数可以不传,则为
undefined
,也可多传,没卵用(也不一定,arguments可以)。- 参数不可同名,如果同名,则后面为准,不要这么干。
- 形参与实参:函数定义的参数
a
为形参,调用是传入的数据3
为实参。 参数设置默认值几种方式:参数赋值(ES6),参数验证赋值。
function func1(a="默认值") { //一般推荐的方式
a=a?a:"默认值"; //参数为null、undefined、0、false、空字符值都会用默认值
a=a||"默认值"; //同上
console.log(a);
}
func1();
func1(3);
func1(3,3,"abc");
参数的值传递和引用传递:取决于参数的类型,值类型传递副本,引用类型传递指针地址,操作的是同一个对象。这里需要理解js里面的值类型、引用类型的基本原理。
arguments:函数传入的实参都保存在arguments对象中,对于任意参数的方法就很有用。
function connect(separator) {
let str = "";
for (let i = 1; i < arguments.length; i++) {
str += arguments[i].toString() + separator;
}
return str.substring(0,str.lastIndexOf(separator)); //去掉结尾的连接符
}
剩余参数(…theArgs):把不确定的参数放到一个数组里,最后一个形参以
**...**
开头。function connect(separator,...arrs) {
let str = "";
for (let i = 0; i < arrs.length; i++) {
str += arrs[i].toString() + separator;
}
console.log(arrs);
return str.slice(0,-1); //去掉结尾的连接符
}
函数调用call/apply/bind
简单的调用方式:
直接函数名调用:
函数名(参数...);
- 对象调用:对象里的函数,
对象.函数名(参数...);
- 递归调用,嵌套调用自身,须注意退出机制,避免死循环,代码的世界没有天荒地老。
🔸[**call**](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/call)(thisArg,arg...)
执行函数:
- 第一个参数指定运行时
this
,这一点可用来实现函数的继承(在构造函数中调用父构造函数)。当第一个参数为null、undefined的时候,默认指向window。 - 后面为函数原本参数。
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price); //继承Product()
this.category = 'food';
}
var cheese = new Food('feta', 5);
🔸[**apply**](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply#%E8%AF%AD%E6%B3%95)(thisArg,argsArray)
执行函数:和call的唯一区别就是第二个参数,是一个数组,数组参数会分别传入原函数。
//利用apply的数组参数传递,可以实现批量传递参数
let max=Math.max.apply(null,[1,2,3,7,4]);
max=Math.max(1,2,3,7,4);
//绑定this
var uname = "sam";
let f = function () {
console.log(this.uname);
}
f(); //sam
f.call({ uname: "call" }); //call
f.apply({ uname: "apply" }); //apply
🔸[**bind**](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)(thisArg,arg...)
复制函数:创建一个副本函数并绑定this和参数,一经绑定,永恒不变(不可更改,不可二次绑定)。
function f1(name,value){
console.log(`${name}:${value},this:${this.f}`);
}
let f2=f1.bind({f:"f2obj"},"f"); //f:undefined,this:f2obj
//f2中绑定了this、第一个参数name
let f3=f2.bind({f:"f3obj"},"f3"); //f:f3,this:f2obj
//f3绑定this无效,参数"f3"绑定到了f2未绑定的第二个参数value上
函数(变量)作用域
- 局部变量:函数内的变量称为“局部变量”,函数里才有作用域的问题——不能被全局、其他函数访问。
- 全局变量:全局变量可以被自由访问。
- 父函数变量:函数中定义的函数也可以访问在其父函数中定义的所有变量和父函数有权访问的任何其他变量,父的最上级就是全局变量对象了。
⁉️注意:var num1=0;
function getScore() {
let num1 = 2, //和全局变量同名
num2 = 3; //var、let、const申明子函数都可以访问
num3=4;
function add() {
num4=5; //没用任何申明的局部变量,使用后自动变为隐式全局变量
var num5=6;
return num1 + num2;
}
return add();
}
getScore(); //5
console.log(num1,num3,num4); //0 4 5
- 全局、局部(方法内)变量同名,两者没有关系,函数内肯定就近用自己的了。
- 没用任何申明的局部变量,使用后自动变为隐式全局变量,so,不要这样干!
闭包(函数)
闭包是函数和声明该函数的词法环境的组合,简单来说能够访问其他函数内部变量的函数,加上他引用的外部变量,组成了闭包。通常就是嵌套函数,嵌套函数可以”继承“容器函数的参数和变量,或者说内部函数包含外部函数的作用域。
- 内部函数+外部引用形成了一个闭包:它可以访问外部函数的参数和变量,但是外部函数却不能使用它的参数和变量。闭包存储了自己和其作用域的变量,这样在函数调用栈上才能使用外部函数的变量。
作用域链(C>B>A):B和A形成闭包,B可以访问A,保存了A的变量;C和B形成B包,可以访问B(也包括B有的A作用域)。function FA(x) {
function FB(y) {
function FC(z) {
console.log(x + y + z);
}
FC(3);
console.dir(FC)
}
FB(2);
console.dir(FB)
}
FA(1); //6
console.dir(FA)
因此,闭包就是为了解决了函数的词法作用域问题,V8引擎是把闭包封装成了一个"Closure"
对象,保存函数上下文中的[[Scope]]
集合里。同一个函数多次调用都会产生单独的闭包,如果闭包使用不当或太多,容易引发内存泄漏风险。JS设计闭包这个东西,真是个坑!详见后续《(函数)执行上下文》
()=>{ }箭头函数
箭头函数(IE🚫)是一种简洁的函数表达式,它没有自己的this
,arguments
,super或new.target。箭头函数总是匿名的,适用于那些需要匿名函数的地方,并且它不能用作构造函数。
语法规则:
(param1, param2, …, paramN) => { statements }
(s => { console.log(s) })("2"); //简单的箭头函数
let FuncA = (a, b) => {
return a + b;
};
this关键字
[**this**](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this)
是JS的关键字,指向当前执行的环境对象,允许函数内引用当前环境的其他变量,不同场景指向不同。在严格模式("use strict";
)下又会不同。大多数情况下,函数的调用方式决定了 this 的值(运行时绑定),每次调用函数的this也可能不同。
:::warning
this指向的是一个对象引用,不是指向函数自身,也不是指向函数的词法作用域。大多数情况下默认都指向全局window
:::
- this=new对象:构造函数中的this指向其新对象;对象的属性方法中的this指向该对象。
- this=全局window:在全局执行环境中(在任何函数体外部)this 都指向全局对象
window
。 - this=调用者:局部(函数内的)this,谁调用函数,this指的就是谁。箭头函数除外,箭头函数本身没有this,也不会接收call、apply的传递,指向其函数定义时的this,而非执行时。
- this=事件元素:在事件中,this表示接收事件的元素。
- this=类class:??
- this=绑定对象:call(thisArg)、apply(thisArg)、bind(thisArg)绑定参数thisArg对象作为其上下文的this,若参数不是对象也会被强制转换为对象性,强扭的瓜解渴!
- this=undefined:严格模式下,如果this没有被执行环境(execution context)定义,那
this
就是undefined
。 ```javascript function Foo() {
}console.log(String(this));//调用Foo(),this指向window;如果new Foo()则指向新对象
var fa = () => { console.log("fa:"+this) };
var fb = function () {
console.log("fb:" + this);
}
fa(); //箭头函数,调用Foo(),this指向调用者window;如果new Foo()则指向新对象
fb(); //匿名函数,调用Foo()、构造函数调用,this指向调用者window,
this.fc1 = fa; //属性方法:this指新对象
this.fc2 = fb; //属性方法:this指新对象
var x=”x1”; var f2=function(){ var x=”x2”; console.log(this.x); //x1 this的变量x,this指向全局上下文对象 console.log(x); //x2 自己的私有变量x } f2();
> 又是一个JS的坑!好像懂了,又好像没懂!详见后续《(函数)执行上下文》
<a name="njVbp"></a>
## 全局函数
| [**eval()**](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval) | 执行JS代码(不推荐)<br />- **比较危险**,它使用与调用者相同的权限执行代码,字符串代码容易被被恶意修改。<br />- **效率低**,它必须先调用JS解释,也没有其他优化,还会去查找其他JS代码(变量)。<br />- **推荐用**`**Function**`**构造函数代替**`eval()`<br /> |
| --- | --- |
| [isNaN()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/isNaN) | 判断一个值是否是NaN,同[Number.isNaN()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN) |
| [parseFloat()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/parseFloat) | 转换字符为浮点数 |
| [parseInt()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/parseInt) | 转换字符为整数 |
| [decodeURI()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/decodeURI) | URL解码 |
| [encodeURI()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/encodeURI) | URL编码 |
| **alert**(str) | 弹窗消息提示框 |
| **confirm**(str) | 弹窗消息询问确认框,返回boolean |
| [console](https://developer.mozilla.org/zh-CN/docs/Web/API/Console) | 控制台输出 |
| console.log(str) | 控制台输出一条消息 |
| console.error(str); | 打印一条错误信息,类似的还有info、warn |
| console.dir(object) | 打印对象 |
| console.trace() | 输出堆栈 |
| console.time(label) | 计时器,用于计算耗时(毫秒):time(开始计时) > timeLog(计时) > timeEnd(结束) |
| console.clear() | 清空控制台,并输出 Console was cleared。 |
```javascript
eval("console.log('eval')");
let arr = eval('[1,2,3]'); //转换字符串为数组
let jobj = eval("({name:'sam',age:22})"); //转换字符串为JSON对象
let jobj2 = new Function("return {name:'sam',age:22}")(); //转换字符串为JSON对象
//计时time,需一个统一标志
console.time("load");
console.timeLog("load"); //load: 5860ms
console.timeLog("load"); //load: 18815ms
console.timeEnd("load"); //load:25798 毫秒 - 倒计时结束
02、{函数}的执行
被JavaScript的闭包、上下文、嵌套函数、this搞得很头痛,这语言设计的怎么这样难懂,感觉比较混乱。看了大量相关资料,先勉强理解一点,总结一下。
执行上下文 (execution context)
是什么? | 执行上下文 (execution context) 是JavaScript代码被解析和执行时所在环境的抽象概念。每一个函数执行的时候都会创建自己的执行上下文,保存了函数运行时需要的信息,并通过自己的上下文来运行函数。 |
---|---|
干什么用的? | 当然就是运行函数自身的,实现自我价值。 |
有那些种类? | ① 全局上下文:默认的最基础的执行上下文,所有不在任何函数内的代码都在这里面。 - 浏览器中的全局对象就是window,全局作用域下var申明、隐式申明的变量都会成为全局属性变量,全局的this指向window。 - 其中会初始化一些全局对象或全局函数,如代码中的console、undefined、isNaN |
② 函数上下文:每个函数都有自己的上下文,调用函数的时候创建。可以把全局上下文可以看成是一个顶级跟函数上下文。
③**eval()**
调用内部上下文:eval
的代码会被编译创建自己的执行上下文,不常用也不建议使用,有些框架会用eval()来实现沙箱Sandbox。 |
| 保存了什么信息? | 初始化上下文的变量、函数等信息
- thisValue:this
环境对象引用
- 内部(Local)环境:上下文的所有变量、函数、参数(arguments)
- 作用域链:具有访问作用域的其他上下文信息。
|
| 到哪里去? | 执行上下文由函数调用栈来统一保存和调度管理。 |
| 生命周期 | 创建(入栈) => 执行=> 销毁(出栈),函数调用的时候创建,执行完成后销毁。 |
函数调用栈
函数调用栈(Function Call Stack),管理函数调用的一种栈式结构(后进先出 ),或称执行栈,存储了当前程序所有执行上下文。最早入栈的理所当然就是程序初始化时创建的全局上下文
了,他是VIP会员,会一直在栈底,直到程序退出。
🔸函数执行上下文调用流程(也是函数的生命周期):
- 入栈:创建执行上下文,并压入栈,获得控制权。
- 执行:执行函数体的代码,给变量赋值?查找变量。如有内部函数调用,递归重复该流程。
- 出栈:函数执行完成出栈,释放该执行上下文,其变量也就释放了。控制权回到上一层执行上下文。
上面的代码执行过程如下图所示:function first() {
second();
}
function second() {
}
first();
- 程序初始化运行时,首先创建的是全局上下文
Global
,进入执行栈。 - 调用
first()
函数,创建其下文并入栈。 first()
函数内部调用了second()
函数,创建其下文入栈并执行。second()
函数执行完成并出栈,控制权回到first()
函数上下文。first()
函数执行完成并出栈,控制权回到全局上下文。
🌰再来一个函数调用栈的示例:
function FA(x) {
function FB(y) {
function FC(z) {
console.log(x + y + z);
}
FC(3);
}
FB(2);
}
FA(1); //6
上面函数在执行FC()
时的函数调用堆栈如下图(断点调试):
:::warning
📢 函数调用栈容量是有限的:所以递归函数时,不停的入栈,递归次数太多会超出堆栈容量限制。
怎么解决呢?拆分执行;用setTimeout(func,0)
发送到任务队列单独执行。
:::
function add(x) {
if (x <= 0)
return 0;
return x + add(x - 1); //递归求和
}
add(1000); //Firefox:1000可以,1500就报错 InternalError: too much recursion
add(10000);//Edge:10000可以执行,11000就报错 Maximum call stack size exceeded
词法作用域
作用域(scope)就是一套规定变量作用范围/权限,并按此去查找变量的规则。包括静态作用域、动态作用域,JavaScript中主要是静态作用域(词法作用域)。
- 🔴静态作用域(就是词法作用域):JavaScript是基于词法作用域来创建作用域的,基于代码的词法分析确定变量的作用域、作用域关系(作用域链)。所以是静态的,就是说函数、变量的作用域是在其申明的时候就已经确定了,在运行阶段不再改变。。
- 🟡动态作用域:基于动态调用的关系确定的,其作用域链是基于运行时的调用栈的。比如
this
,一般就是基于调用来确定上下文环境的。因此,词法作用域主要作用是规定了变量的访问权限,确定了如何去查找变量,基本规则:
- 变量(包括函数)申明的的地方决定了作用域,跟在哪调用无关。
- 函数(或块)可以访问其外部的数据,如果嵌套多层,则递归拥有父级的作用域权限,直到全局环境。
- 只有函数可以限定作用域,不能被上级、外部其他函数访问。
- 如果有和上级同名的变量,则就近使用,先找到谁就用谁。
- 变量的查找规则就是先内部,然逐级往上,直到全局环境,如果都没找到,则变量
undefined
。
🔸那词法作用域是怎么实现的呢?——作用域链
function FA(x) {
function FB(y) {
x+=y;
console.log(x);
}
console.dir(FB);
return FB; //返回FB()函数
}
let fb = FA(1); //FA函数执行完成,出栈销毁了
fb(2); //3 //返回的fb()函数保留了他的父级FA()作用域变量x
fb(2); //5
fb(2); //7 //同一个闭包函数重复调用,内部变量被改变
父级函数执行完成后就出栈销毁了(典型场景就是返回函数,可以到任何地方执行),那内部函数执行的时候到哪里去找父级函数的变量呢?
- ✅ 函数内部作用域:首先每个函数执行都会创建自己作用域(执行上下文),代码执行的时候优先本地作用域查找。
- ✅ 闭包:引用的外部(词法上级)函数作用域就形成了一个闭包,用一个
**Closure**
(Closure /ˈkləʊʒə(r)/ 闭包)对象保存(引用的变量),多个(引用)逐级保存到函数上下文的**[[Scope]]**
(Scope /skoʊp/ 作用域)集合上,形成作用域链。 - ✅ 作用域链的最底层就是指向全局对象的引用,她始终都在,不管你怎么对她;最上层就是正在执行的上下文。
✅ 变量查找就在这个作用域链进行:自己上下文(词法环境,变量环境) => 作用域链逐级查找=> 全局作用域 =>
**undefined**
:::warning 📢闭包简单理解就是,当前环境中存在指向父级作用域的引用。如果嵌套子函数完全没有引用父级任何变量,就不会产生闭包。不过全局对象是始终存在其作用域链[[Scope]]上的。 ::: 🌰举个例子:var a = 1;
let b = 2;
function FunA(x) {
let x1 = 1;
var x2 = 2;
function FunB(y) {
console.log(a + b + x + x1 + x2 + y);
}
FunB(2);
console.dir(FunB)
}
FunA(1); //9
console.dir(FunA)
上面的代码示例中,
FunA()
函数嵌套了FunB()
函数,如下图FunB()
函数的[[Scope]]
集合上有三个对象:**Closure (FunA)**``FunA()
函数的闭包,包含他的参数、私有变量。**Script**
:Script Scope 脚本作用域(可以当做全局作用域的一部分),存放当前Script内可访问的let、const变量,就是全局作用域内的变量。var
变量a
被提升为了全局对象的“属性”了。**Global**
:全局作用域对象,就是window,包含了被var
提升的变量a
。
如果把FunB()
函数放到外面申明,只在FunA()
调用,其作用域链就不一样了。
执行上下文的创建
执行上下文的创建过程中会创建对应的词法作用域,包括词法环境和变量环境。
- 创建词法环境(LexicalEnvironment):
- 环境记录
EnvironmentRecord
:记录变量、函数的申明等信息,只存储函数声明和let/const声明的变量。 - 外层引用
outer
:对(上级)其他作用域词法环境的引用,至少会包含全局上下文(当然除了全局本身)
- 环境记录
创建变量环境(VariableEnvironment):本质上也是词法环境,只不过他只存储var申明的变量,其他都和词法环境差不多。
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
:::warning ❗变量查找:变量查找的时候,是先从词法环境中找,然后再到变量环境。就是优先查找const、let变量,其次才var变量。 :::
换几个角度来总结下,创建执行上下文主要搞定下面三个方面:
① 确定 this 的值(This Binding):在全局上下文中,this指向window。
- 函数执行上下文中,如果它被一个对象引用调用,那么 this 的值被设置为该对象,否则 this 的值被设置为全局对象或 undefined(严格模式下)
- call(thisArg)、apply(thisArg)、bind(thisArg)会直接指定thisValue值。
② 内部环境:自己函数内部的变量、函数等信息,还有参数arguments
对象。
③ 作用域链(外部引用):外部的词法作用域存放到函数的[[Scope]]集合里,用来查找作用域变量。
❓有什么结论?
- 变量越近越好:最好都本地化,尽量避免让变量查找链路过长,一层层切换作用域去找是很累的。
- 优先
const
,其次let
,尽量不用var
。 - 注意函数调用堆栈的长度,比如递归。
- 闭包函数使用完后,手动释放一下,
fun = null;
远离JavaScript…我以为已经学会了,其实可能还没进去。