(JavaScript - 原生)函数
如果说JavaScript中变量是公民的话,那么我认为函数就是皇上!这一章就来一起研究JavaScript中的函数,对于这个无论是拜将封侯的老鸟还是冲锋陷阵的菜鸟都爱不释手的皇上,特此记录下来以备查看,希望本次讲解真正做到深入浅出!
插曲:
对于我们后续的讲解,我们先来了解一个概念 - 变量提升 -> 仅仅存在于ES5及其之前的ECMA的语言规范中,ES6废弃这一传统。
变量提升就是指在js代码的编译期间,js引擎将会将所有的变量拿出来进行初始化。
console.log(num);// 打印 undefined
var num = 1;
打印结果:
很多人有疑问,为啥num明明都赋值了还会打印undefined?请注意打印的位置,在变量声明之前打印此变量就会被js引擎附上默认值undefined。并且这一行为恰恰就反映了,在js代码编译期间进行变量提升,将num变量拿出来赋值默认值undefined,等到js引擎执行到这句的时候才会进行真正的赋值操作。
总而言之:就是在js代码执行之前js引擎会先将js代码中的所有变量拿出来进行初始化(赋默认值),然后等到js引擎执行到相应位置的时候才会进行变量真正的赋值。所以在赋值以后访问这个变量就会显示正常的样子。
var num = 1;
console.log(num);// 打印 1
打印结果:
函数具有块级作用域
流程:
-1).什么是函数?
就是具有特定功能的代码集合。
## 定义函数的方式:
-A).函数的声明
// -1).函数的声明(直接通过function 函数名(){...}的形式来定义函数)
foo();// 打印 window foo ...
function foo() {
console.log("window foo ...");
}
foo();// 打印 window foo ...
打印结果:
可见声明式创建的函数,在函数体之前执行依然可行,就是因为变量提升的概念,只不过这次不仅仅只是提升foo函数的函数名还会立即将其初始化,就是说通过函数的声明这种方式创建的函数会在变量提升的时候就完成真正的赋值操作,等到其它变量则要等到函数具体执行到相应位置的时候才会进行真正的赋值操作,所以在第一个 foo()执行之前foo这个函数就已经被初始化完毕,故会正常执行不会报错。
-B).函数表达式
// -2).函数表达式
fun();// 报错 fun is not defined ...
let fun = function() {
console.log("window fun ...");
};
打印结果:
为什么会这样呢?foo可以在函数体之前执行函数,为什么fun就不可以呢?
因为foo这种函数的声明的方式创建的函数会存在变量提升,即在编译的时候会先将foo整个函数放到执行环境中去进行初始化,所以foo函数体是先foo()执行的,所以不会报错且能正常执行。
但是fun属于函数表达式方式创建的函数,其变量提升的时候不是提升的整个函数,而是将变量fun进行初始化即为其开辟空间赋默认值undefined,带到代码执行到赋值号(=)的时候,才会将其后面的函数初始化并将函数的指针赋值给fun变量,所以函数是后于fun();执行的,所以你先去执行fun()肯定会报错fun is not defined ...
但是如果你将执行语句放在函数定义的下面执行就会正常执行,因为此时fun已被真正赋值 ...
// -2).函数表达式
let fun = function() {
console.log("window fun ...");
};
fun();// 打印 window fun ...
打印结果:
-C).构造器式
// -3).构造器式
let constructor = new Function("num1", "num2", "console.log('ok');console.log('sum:' + (num1 + num2))");
constructor(1, 2);// 打印 ok sum:3
打印结果:
构造器创建的对象相比较前两种方式过于复杂,其核心是,最后一个参数之前的所有参数都是函数的入参(形参)而最后一个参数字符串就相当于函数体。这样定义函数过于麻烦,不推荐使用。
-2).为什么使用函数?
-A).代码复用
假如我们接了一个任务,打印动物名字。
不使用函数的情况下:
第一次打印小猫,第二次打印小狗如果第三次还要打印小猫的话就还得重新写打印语句。
// 打印小猫
console.log(`小猫`);
console.log(`小狗`);
console.log(`小猫`);
打印结果:
使用函数的情况下:
不用每次重新写打印语句,而是在需要打印的地方直接传参调用函数即可,方便又高效。
function print(name) {
console.log(name);
}
print(`小猫`);
print(`小狗`);
print(`小猫`);
打印结果:
-B).防止变量污染
不使用函数的时候所有的变量全都暴露在全局作用域下,容易造成变量的污染。
eg:
var arg = "a";
var arg = "b";
console.log(arg);// 打印 b
打印结果:
使用函数的情况下:
var arg = "a";
console.log(arg);// 打印 a
function foo() {
var arg = "b";
console.log(arg);// 打印 b
}
foo();
打印结果:
-3).怎样使用函数?
> 函数的返回值
在JavaScript中不管我们指没指定函数的返回值,函数都会返回一个值
指定返回值 -> 返回指定的值
未指定返回值 -> 返回undefined
let res_1 = function back1() {
return "1";
};
console.log(res_1());// 打印 1
let res_2 = function back2() {
};
console.log(res_2());// 打印 undefined
打印结果:
> 函数可以作为参数传递给另一个函数:
// 函数作为对象也可以当做实参传给其它函数
function eat(food) {
return `吃:${food}`;
}
person("苹果", eat);
function person(food, eat) {
console.log(`人${eat(food)}`);// 打印 人吃:苹果
}
打印结果:
>函数的内部属性(arguments、this)
①.arguments形参的集合(每个函数都具备的属性,是一个类数组用来访问形参)
// arguments => 形参的集合
function watch() {
console.log(`我是数组吗?${arguments instanceof Array}`);// 打印 我是数组吗?false
console.log(`arguments的长度:${arguments.length},第一个参数为:${arguments[0]}`);// 打印 arguments的长度:3,第一个参数为:电视剧
}
watch("电视剧", "电影", "动漫");
打印结果:
②.this谁调用我所在的方法,我就指向谁
// -1).场景一:普通函数
严格模式下:
// -1).场景一:普通函数
function walk() {
console.log(`walk this指向了:${this}`);// 打印 walk this指向了:undefined
}
walk();
因为ES6默认使用严格模式所以this指向了undefined,在非严格模式下,即ES5及ES5之前的语法此时的这个this指向的是全局window对象。
打印结果:
非严格模式下:
// -1).场景一:普通函数
function walk() {
console.log(`walk this指向了:${this}`);// 打印 walk this指向了:window
}
walk();
打印结果:
// -2).场景二:构造函数
// -2).场景二:构造函数
function run(address, speed) {
this.address = address;
this.speed = speed;
this.sayThis = function() {
console.log(`run this指向了:${this},this是否指向构造器的外部实例${this === obj},地址:${this.address},速度:${this.speed}`);// 打印 run this指向了:[object Object],this是否指向构造器的外部实例true,地址:承德市,速度:20
}
}
let obj = new run("承德市", 20);
obj.sayThis();
构造器中的this指向的是外部实例
打印结果:
// -3).场景三:事件绑定
// -3).场景三:事件绑定
let btn = document.getElementsByTagName("button")[0];
btn.addEventListener('click', function() {
console.log(`this指向:${this}`);// 打印 this指向:[object HTMLButtonElement]
}, false);
事件绑定中的this指向被绑定事件的对象上
打印结果:
总而言之记住一句话:谁调用我所在的方法,我就指向谁!
上面3个案例中,第一个是在全局作用域中执行的,调用者就是window,第二个案例是在new 构造器创建对象的时候执行的,所以调用者就是new 完的结果就是该实例,第三个案例是为DOM元素绑定事件,所以就指向了该DOM元素。
> JavaScript中么没有函数函数重载的概念
// 四、JavaScript中的函数没有重载
function talk(language) {
console.log(`语言:${language}`);
}
talk("未知");
function talk(dream) {
console.log(`行为:${dream}`);
}
console.log(talk === talk);// 打印 true
// 打印 行为:未知
打印结果:
为什么最上面的talk方法没有被调用呢?因为在JavaScript中函数的名字是函数在堆中的指针,两个talk都指向指定堆中的同一个函数对象,且都对它进行修改,所以必定以最后更改的为准,所以会发生覆盖现象,诸如像Java中的重载,每个重载的方法都有自己的空间,而JavaScript中只要函数名重名就会发生覆盖现象,所以JavaScript没有方法重载的概念。
## 但是我们可以借助arguments来模拟实现函数的重载。
// 但是我们可以借助arguments来模拟实现函数的重载。
function overLoad() {
if(arguments.length === 1 && typeof arguments[0] === 'number') {
console.log(`数字为:${arguments[0]}`);
}else if(arguments.length === 1 && typeof arguments[0] === 'string') {
console.log(`字符串为:${arguments[0]}`);
}else if(arguments.length === 2) {
console.log(`一参是:${arguments[0]},二参是:${arguments[1]}`);
}
}
overLoad(101);// 打印 数字为:101
overLoad("嘻哈");// 打印 字符串为:嘻哈
overLoad(1, "小天才");// 打印 一参是:1,二参是:小天才
// 如此通过arguments对象来把控形参的个数与类型就可以进行模拟重载 ...
打印结果:
> 设置函数参数的默认值
## 假如我们有形参,但并没有传递实参我们该怎么办?
设置默认值:
ES5及其之前的方法:
// ES5及其之前的默认值设置
function defaultValue(x, y) {
if(typeof y === 'undefined' || Number.isNaN(Number(y))) {
y = 2;
}
y = Number(y);
return x + y;
}
console.log(defaultValue(1, ""));
打印结果:
ES6设置默认值的方法:
// ES6的默认值设置
function defaultValue(x, y = 2) {
return x + y;
}
console.log(defaultValue(1));// 打印 3
打印结果:
> 函数的属性和方法
-1).length属性 => 获取除了设置默认值的形参的个数
// -1).length属性 -> 函数形参非设置默认值的参数个数
function sleep(arg_1, arg_2, arg_3 = 3) {
console.log(`${arg_1}只羊,${arg_2}只羊,${arg_3}只羊`);// 打印 1只羊,2只羊,3只羊
console.log(`函数参数的长度(不包括设置默认值的参数个数):${sleep.length}`);// 打印 函数参数的长度(不包括设置默认值的参数个数):2
}
sleep(1, 2);
打印结果
按理说我只传递了两个参数,应该打印1只羊,2只羊,undefined只羊才对呢?因为咱们在形参arg_3处看见了其设置了默认值为3,故只要为arg_3赋值undefined就会触发默认值 ...
* length属性可以获取形参个数但不包括已经设置默认值的形参。
-2).name属性 => 获取指定函数的函数名
function write() {
console.log(`函数名字是:${write.name}`);
}
write();// 打印 函数名字是:write
打印结果:
-3).prototype属性 => 不再赘述如想了解更多请参看我的这篇博文创建对象的方式 + 原型 + 原型链 + 对象的继承
-4).apply、call、bind方法
这三个方法都是可以改变指定函数的this即可以扩展指定函数作用域,apply与call方法的1参都是改变的this对象,apply方法的第二参数为一个参数数组作为调用函数的实参,call方法在第一参数后可以接收任意个参数作用同apply的参数数组。
// -3).apply方法与call方法和bind方法
// 这两个方法都可以对已有函数扩展作用域。
// apply(...)与call(...)方法第一个参数都一样,都是this,剩下的apply方法第二参数为一个数组作为实参,call方法第二及其以后参数作为实参
window.color = 'red';
let o = {
color: 'green'
};
function change(name) {
console.log(`${name}的颜色:${this.color}`);
}
change.apply(window, ["赵云"]);// 更改change函数的this为window // 打印 赵云的颜色:red
change.call(o, "张飞");// 更改change函数的this为o // 打印 张飞的颜色:green
// 如此我们便通过apply与call成功的将原函数的作用域扩展了。
// -4).bind方法
let new_foo = change.bind(window);
new_foo();// 更改change函数的this为window // 打印 undefined的颜色:red
打印结果:
-4).闭包
说到闭包我们先介绍一个知识点叫做作用域链。
-A).作用域链:
// 八、函数的作用域
let animal = "bird";
function fly() {
console.log(animal);// 打印 bird
}
fly();
为什么在fly函数中可以使用animal变量呢?可能你会说因为animal是在全局定义的呀,属于全局变量。那好我问你,你知道具体细节吗?
来,先看张图
-B).闭包:
什么是闭包?
闭包是指有权访问另一个函数作用域中的变量的函数。
// 九、闭包
let a = function swim() {
let apple = "苹果";
return function() {
return apple;
}
};
console.log(a()());// 打印 苹果
打印结果:
如此swim中返回的匿名函数就是swim函数的闭包。
但是使用闭包可能会导致内存泄露!为什么呢?因为请看上面代码,swim函数的闭包一直被变量a所引用着,而变量a又在全局作用域下,所以只有当页面关闭全局作用域销毁的时候,变量a的引用才会被销毁,否则一直不会被回收,试想一下如果大量的使用闭包会对浏览器造成极大的负荷就有可能导致内存泄漏。虽然我们可以通过a = null;来手动解除引用但是还是那句话,如果大量使用闭包的话,你难道还会一个一个的手动解除吗?所以尽量少用闭包。
再来看看一个关于闭包的案例:(我们通常为一组DOM元素绑定事件的时候可能会遇到)
let oLi = document.getElementsByTagName("li");
for(var i = 0;i < oLi.length;i++) {
oLi[i].addEventListener('click', function() {
console.log(i);
}, false);
}
打印结果
不管点击哪一个li结果都打印6
这个结果肯定不是咱们想要的,咱们要的是点击对应的li就打印对应的索引,但是为什么会出现这种情况呢?
就是因为i是全局作用域下的变量对象的引用,而addEventListener里边的回调函数打印的i用的也是全局作用域下的变量对象引用的i,当for循环执行完毕后i就等于6,所以凡是用到全局作用域的变量对象引用的i的地方就都会变成6,此时我们在做点击的时候打印的就都是6了。
通过闭包来改进:
let oLi = document.getElementsByTagName("li");
for(var i = 0;i < oLi.length;i++) {
(function(num) {
oLi[i].addEventListener('click', function() {
console.log(num);
}, false);
})(i)
}
我们在点击事件的外面加了一层闭包,通过(i)将每次的i的值复制给num,由于Number类型是基本类型,所以i变动不影响num。整个代码就相当于,类数组oLi的每一项都存储着一个addEventListener属性,而这个属性存储的i值就是每次传入进来num的值,故这样一来就保证了我们要的i值的唯一性。
打印结果:
点击对应的li就会打印对应的索引
总结:JavaScript中的函数是我们使用频率比较高的,而且JavaScript中只有函数有块级作用域,但是在ES6中为JavaScript增加了块级作用域。整个函数部分比较难理解的地方就要数闭包这块了,所以想要弄明白闭包,就得先弄明白作用域链,闭包用好了可以解决很多实际问题,但不能滥用虽然V8引擎已经支持会回收闭包占用的资源但是我们也要养成良好习惯,因为并不是所有浏览器都实现了这一功能。
本次探究就到这里啦,如果有什么意见或者建议请在下方留言,我会在第一时间回复!谢谢各位!