函数
简介
什么是函数
-
什么是函数? 函数是专门用于封装代码的, 函数是一段可以随时被反复执行的代码块
-
函数格式
function /*函数名称*/(/*形参列表*/){ /*被封装的代码*/; } -
不使用函数的弊端
- 冗余代码太多
- 需求变更, 需要修改很多的代码
-
使用函数的好处
- 冗余代码变少了
- 需求变更, 需要修改的代码变少了
函数的定义步骤
- 书写函数的固定格式
- 给函数起一个有意义的名称
- 为了提升代码的阅读性
- 函数名称也是标识符的一种, 所以也需要遵守标识符的命名规则和规范
- 确定函数的形参列表, 看看使用函数的时候是否需要传入一些辅助的数据
- 将需要封装的代码拷贝到{}中
- 确定函数的返回值 可以通过return 数据; 的格式, 将函数中的计算结果返回给函数的调用者
函数的注意点
-
一个函数可以有形参也可以没有形参(零个或多个) 定义函数时函数()中的变量我们就称之为形参
// 没有形参的函数 function say() { console.log("hello world"); } say(); // hello world// 有形参的函数 function say(name) { console.log("hello " + name); } say("fhs"); // hello fhs -
一个函数可以有返回值也可以没有返回值
// 没有返回值的函数 function say() { console.log("hello world"); } say();// 有返回值的函数 function getSum(a, b) { return a + b; } let res = getSum(10 , 20); console.log(res); // 30 -
函数没有通过 return 明确返回值, 默认返回 undefined
function say() { console.log("hello world"); return; } let res = say(); console.log(res); // undefined -
return的作用和break相似, 所以return后面不能编写任何语句 (永远执行不到) break作用立即结束switch语句或者循环语句 return作用立即结束当前所在函数
function say() { console.log("hello world"); return; console.log("return后面的代码"); } say(); // hello world -
调用函数时实参的个数和形参的个数可以不相同 调用函数时传入的数据我们就称之为实参
-
JavaScript中的函数和数组一样, 都是引用数据类型 (对象类型) 既然函数是一种数据类型, 所以也可以保存到一个变量中 将来可以通过变量名称找到函数并执行函数
let say = function () { console.log("hello world"); } say(); // hello world
函数与参数
参数 arguments
-
因为 console.log(); 也是通过 () 来调用的, 所以 log 也是一个函数
-
log 函数的特点 可以接收 1 个或多个参数
-
为什么 log 函数可以接收 1 个或多个参数 内部的实现原理就用到了 arguments
-
arguments 的作用 保存所有传递给函数的实参
-
每个函数中都有一个叫做arguments的东西, arguments其实是一个伪数组
function getSum() { let sum = 0; for (let i = 0; i < arguments.length; i++){ let num = arguments[i]; sum += num; } return sum; } let res = getSum(10, 20, 30, 40); console.log(res); // 100函数括号内每个参数都被放进了 arguments 对象(伪数组)里
// 补充 function test(a) { console.log(arguments); } test(0, 1, 2, 3, 4, 5); // [Arguments] { '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5 }
扩展运算符
-
扩展运算符在等号左边, 将剩余的数据打包到一个新的数组中 注意点: 只能写在最后 let [a, ...b] = [1, 3, 5]; a = 1; b = [3, 5];
-
扩展运算符在等号右边, 将数组中的数据解开
let arr1 = [1, 3, 5]; let arr2 = [2, 4, 6]; let arr = [...arr1, ...arr2]; let arr = [1, 3, 5, 2, 4, 6]; -
扩展运算符在函数的形参列表中的作用 将传递给函数的多出来的所有实参打包到一个数组中 注意点: 和在等号左边一样, 也只能写在形参列表的最后
function getSum (a, ...values) { console.log(a); console.log(values); } getSum(10, 20 , 30); // 10 // 20, 30
函数形参默认值
在 ES6 之前可以通过逻辑运算符来给形参指定默认值 格式: 条件A || 条件B
- 如果条件 A 成立, 那么就返回条件 A
- 如果条件 A 不成立, 无论条件 B 是否成立, 都会返回条件B
function getSum(a, b) {
a = a || "211";
b = b || "996";
console.log(a, b);
}
getSum(123, "abc"); // 123, "abc" (妈欸这里控制台又有引号, 纯字符串又没引号, 薛定谔的引号, 非常淦)
从 ES6 开始, 可以直接在形参后面通过 = 指定默认值 (和解构赋值的指定默认值呼应上了) 注意点: ES6 开始的默认值还可以从其它的函数中获取
function getSum(a = "211", b = getDefault()) {
console.log(a, b);
}
getSum();
function getDefault() {
return "996"
}
函数作为返回值
- 将函数赋给其他变量
let say = function () {
console.log("hello world");
}
let fn = say;
fn();
- 将函数作为其他函数的参数
let say = function () {
console.log("hello world");
}
function test(fn) { // 相当于 let fn = say;
fn();
}
test(say);
- 将函数作为其他函数的返回值
function test() {
// 注意点: 在其它编程语言中函数是不可以嵌套定义的, 但是在JavaScript中函数是可以嵌套定义的
let say = function () {
console.log("hello world");
}
return say;
}
let fn = test(); // 相当于 let fn = say;
fn();
注意点: 在其它编程语言中函数是不可以嵌套定义的, 但是在JavaScript中函数是可以嵌套定义的
花哨的函数
匿名函数
- 什么是匿名函数? 匿名函数就是没有名称的函数
- 匿名函数的注意点 匿名函数不能够只定义不使用
- 匿名函数的应用场景
- 作为其他函数的参数
- 作为其他函数的返回值
- 作为一个立即执行的函数
- 有名称的函数
function say() {
console.log("hello fhs");
}
let say = function() {
console.log("hello fhs");
}
- 匿名函数
function() {
console.log("hello fhs");
}
- 匿名函数作为其他函数的参数
function test(fn) { // let fn = say;
fn();
}
test(function () {
console.log("hello world");
});
- 匿名函数作为其他函数的返回值
function test() {
return function () {
console.log("hello fhs");
};
}
let fn = test(); // let fn = say;
fn();
- 匿名函数作为一个立即执行的函数 注意点: 如果想让匿名函数立即执行, 那么必须使用()将函数的定义包裹起来才可以
(function () {
console.log("hello it666");
})();
箭头函数
-
什么是箭头函数?
-
箭头函数是ES6中新增的一种定义函数的格式
-
目的: 就是为了简化定义函数的代码, 给一套花里胡哨的关于 this 的用法
-
let arr = new Array(); let arr = [];
-
-
在 ES6 之前如何定义函数
function 函数名称(形参列表){ /*需要封装的代码*/; } let 函数名称 = function(形参列表){ /*需要封装的代码*/; } -
ES6 新增的函数定义方式
let 函数名称 = (形参列表) =>{ /*需要封装的代码*/; } -
箭头函数的注意点:
-
在箭头函数中如果只有一个形参, 那么 () 可以省略
// function say() { // console.log("hello lnj"); // } let say = () => { console.log("hello lnj"); } say(); -
在箭头函数中如果 {} 中只有一句代码, 那么{}也可以省略
// function say(name) { // console.log("hello " + name); // } // let say = (name) => { // console.log("hello " + name); // } // let say = name => { // console.log("hello " + name); // } let say = name => console.log("hello " + name); // 花里胡哨 say("ccc"); -
当箭头函数的函数体只有一个
return语句时,可以省略return关键字和方法体的花括号var elements = [ 'Hydrogen', 'Helium', 'Lithium', 'Beryllium' ]; elements.map(element => element.length); // [8, 6, 7, 9] -
如果箭头函数的函数体只有一句代码,就是返回一个对象,可以像下面这样写:
// 用小括号包裹要返回的对象,不报错 let getTempItem = id => ({ id: id, name: "Temp" }); // 但绝不能这样写,会报错。 // 因为对象的大括号会被解释为函数体的大括号 let getTempItem = id => { id: id, name: "Temp" }; -
箭头函数和正统函数的 this 不同
-
箭头函数中的 this, 是父作用域的 this, 而非调用者
-
箭头函数的 this 无法变更
-
对象不会成为箭头函数的 this
let p = { name: "fhs", say: function () { console.log(this); }, // 因为没有将箭头函数放到其它的函数中, 所以箭头函数属于全局作用域 // 在 JS 中只有定义一个新的函数才会开启一个新的作用域 (害, 块呢???) hi: () => { console.log(this); } } p.say(); // {name: "fhs", say: ƒ} p.hi(); // Window console.log(this); // Window- 函数会成为箭头函数的 this
function Person() { this.name = "fhs"; this.say = function () { console.log(this); } // 因为将箭头函数放到其它的函数中, 所以箭头函数属于其它函数(当前的其它函数就是构造函数) // 既然箭头函数属于构造函数, 所以箭头函数中的 this 就是构造函数的 this this.hi = () =>{ console.log(this); } } let p = new Person(); p.say(); // Person p.hi(); // Person- 箭头函数的 this 无法变更
function Person() { this.name = "fhs"; this.say = function () { console.log(this); } this.hi = () =>{ console.log(this); } } let p = new Person(); p.say.call({name: "zs"}); // {name: "zs"} /* 注意点: 箭头函数中的 this 永远都只看它所属的作用域的 this 无法通过 bind/call/apply 来修改 */ p.hi.call({name: "zs"}); // Person- 类又不同了, 暴躁
class Person{ constructor(name){ this.name = name; } say = function () { console.log(this); } hi = () =>{ console.log(this); } } let a = new Person("fhs"); console.log(a.say()); // undefined console.log(a.hi()); // undefined- 这儿么写 this 也还是 undefined
class Person{ constructor(name){ this.name = name; this.say = function(){ console.log(this); } this.hi = () =>{ console.log(this); } } } let a = new Person("fhs"); console.log(a.say()); // undefined console.log(a.hi()); // undefined -
-
这里有个挺重要的知识点: this 是 this, 作用域链是作用域链, 俩有关系但不是一回事儿
递归函数
- 什么是递归函数? recursion
- 递归函数就是在函数中自己调用自己, 我们就称之为递归函数
- 递归函数在一定程度上可以实现循环的功能
- 递归函数的注意点:
- 每次调用递归函数都会开辟一块新的存储空间, 所以性能不是很好
- 函数执行完毕后会回到函数调用的地方
// let pwd = -1;
// do{
// pwd = prompt("请输入密码");
// }while (pwd !== "123456");
// alert("欢迎回来");
function login() {
// 1.接收用户输入的密码
let pwd = prompt("请输入密码");
// 2.判断密码是否正确
if(pwd !== "123456"){
login();
}
// 3.输出欢迎回来
alert("欢迎回来"); // 输错几次, 在输入正确密码后就会多弹相同次的 欢迎回来
}
login();
作用域与作用域链
背景介绍
在JavaScript中定义变量有两种方式
- ES6 之前: var 变量名称;
- ES6 新增: let 变量名称;
var 与 let 区别
- 是否能重复定义
- 通过 var 定义变量,可以重复定义同名的变量,并且后定义的会覆盖先定义的
- 如果通过 let 定义变量, "相同作用域内"不可以重复定义同名的变量
- 是否能先使用后定义
- 通过 var 定义变量, 可以先使用后定义(预解析)
- 通过 let 定义变量, 不可以先使用再定义(不会预解析)
- 是否能被 {} 限制作用域
- 无论是 var 还是 let 定义在 {} 外面都是全局变量
- 将 var 定义的变量放到一个单独的 {} 里面, 还是一个全局变量
- 将 let 定义的变量放到一个单独的 {} 里面, 是一个局部变量
全局, 局部(函数), 块级作用域
- 在 JavaScript 中 {} 外面的作用域, 我们称之为全局作用域
- 在 JavaScript 中函数后面 {} 中的的作用域, 我们称之为"局部作用域", 或“函数作用域”
- 在 ES6 中只要 {} 没有和函数结合在一起, 就是"块级作用域" 块级作用域只能约束 let
- 块级作用域和局部作用域区别 (气死人的绕)
- 在块级作用域中通过 var 定义的变量是全局变量
- 在局部作用域中通过 var 定义的变量是局部变量
- 无论是在块级作用域还是在局部作用域, 省略变量前面的 let 或者 var 就会变成一个全局变量 (严格模式不给省略)
- 块级作用域 (选择结构和循环结构都不是函数)
{
// 块级作用域
}
if(false){
// 块级作用域
}
switch () {
// 块级作用域
}
while (false){
// 块级作用域
}
for(;;){
// 块级作用域
}
do{
// 块级作用域
}while (false);
- 函数作用域 (局部作用域)
function say() {
// 局部作用域
}
- var 的辨析
{
// 块级作用域
var num = 123; // 全局变量
}
console.log(num); // 123
function test() {
var value = 666; // 局部变量
}
test();
console.log(value); // 报错
- 块级作用域中的 var 与 let
{
// var num = 678; // 全局变量
// let num = 678; // 局部变量
num = 678; // 全局变量, 严格模式下这个会报错, 引起舒适
}
console.log(num);
- 局部(函数)作用域中的 var 与 let
function test() {
// var num = 123; // 局部变量
// let num = 123; // 局部变量
num = 123; // 全局变量, 严格模式下这个会报错, 引起舒适
}
test();
console.log(num);
思考: let 相比 var 灵活度被大大削弱, 好用程度却大幅上升, 这就是规矩的好处
注意点:
- 在不同作用域范围内可以出现同名变量 (可以但极不推荐)
- 只要出现 let, 相同作用域内就不能出现同名的变量, 即使另一个是 var 也不行
作用域链
全局作用域也称 0 级作用域
注意点: 初学者在研究"作用域链"的时候最好将 ES6 之前和 ES6 分开研究
ES6 之前的作用域链
- 需要明确:
- ES6 之前定义变量通过 var
- ES6 之前没有块级作用域, 只有全局作用域和局部作用域
- ES6 之前函数大括号外的都是全局作用域
- ES6 之前函数大括号中的都是局部作用域
- ES6之前作用域链
- 全局作用域我们又称之为 0 级作用域
- 定义函数开启的作用域就是 1级/ 2级/ 3级/...作用域
- JavaScript会将这些作用域链接在一起形成一个链条, 这个链条就是作用域链 0 ---> 1 ----> 2 ----> 3 ----> 4
- 除0级作用域以外, 当前作用域级别等于上一级 +1
- 变量在作用域链查找规则
- 先在当前找, 找到就使用当前作用域找到的
- 如果当前作用域中没有找到, 就去上一级作用域中查找
- 以此类推直到0级为止, 如果 0 级作用域还没找到, 就报错
// 全局作用域 / 0级作用域
var num = 123;
function demo() {
// 1级作用域
var num = 456;
function test() {
// 2级作用域
var num = 789;
console.log(num);
}
test();
}
demo(); // 789
ES6 往后的作用域链
- 需要明确:
- ES6 定义变量通过 let
- ES6 除了全局作用域、局部作用域以外, 还新增了块级作用域
- ES6 虽然新增了块级作用域, 但是在块级作用域中通过 let 定义变量与函数作用域并无差异 (都是局部变量)
- ES6 作用域链
- 全局作用域我们又称之为 0 级作用域
- 定义函数或者代码块都会开启的作用域就是 1级/ 2级/ 3级/...作用域 (这里就是 ES6 之后的关键不同)
- JavaScript 会将这些作用域链接在一起形成一个链条, 这个链条就是作用域链 0 ---> 1 ----> 2 ----> 3 ----> 4
- 除 0 级作用域以外, 当前作用域级别等于上一级 +1
- 变量在作用域链查找规则
- 先在当前找, 找到就使用当前作用域找到的
- 如果当前作用域中没有找到, 就去上一级作用域中查找
- 以此类推直到0级为止, 如果0级作用域还没找到, 就报错
// 全局作用域 / 0级作用域
let num = 123;
{
// 1级作用域
let num = 456;
function test() {
// 2级作用域
// let num = 789;
console.log(num);
}
test(); // 456
}
预解析, 方法
预解析
- 什么是预解析?
- 浏览器在执行JS代码的时候会分成两部分操作:预解析以及逐行执行代码
- 也就是说浏览器不会直接执行代码, 而是加工处理之后再执行,
- 这个加工处理的过程, 我们就称之为预解析
- 预解析规则
- 将变量声明和函数声明提升到当前作用域最前面
- 将剩余代码按照书写顺序依次放到后面
- 注意点:
- 通过 let 定义的变量不会被提升(不会被预解析)
变量的预解析
- var 声明的变量的预解析
// 预解析之前
console.log(num); //undefined
var num = 123;
// 预解析之后
var num;
console.log(num); //undefined
num = 123;
- 通过 let 定义的变量不会被提升(不会被预解析)
函数的预解析
- 正统函数的预解析
// ES6之前定义函数的格式
say();
// ES6之前的这种定义函数的格式, 是会被预解析的, 所以可以提前调用
function say() {
console.log("hello 996");
}
// 预解析之后的代码
function say() {
console.log("hello 996");
}
say();
/*
在其它语言中, 函数的声明是:
function say()
在 Javascript 中, 函数的声明是
function say() {
console.log("hello 996");
}
*/
- 将函数赋给 var 变量的方式不预解析 (let 更不会啦)
console.log(say); // undefined
say(); // say is not a function
var say = function() {
console.log("hello itzb");
}
// 预解析之后的代码
console.log(say); // undefined
var say;
say(); // say is not a function
say = function() {
console.log("hello itzb");
}
- 箭头函数也不预解析 (都是报错, 这个和 var 报的还不一样, 魔鬼在细节中)
say(); // say is not defined
let say = () => {
console.log("hello itzb");
}
狗题目
注意点: 浏览器执行的是预解析之后的代码
- 预解析规则很简单, 搞顺顺
var num = 123;
fun(); // undefined
function fun() {
console.log(num);
var num = 666;
}
/*
预解析之后:
var num;
function fun() {
var num;
console.log(num);
num = 666;
}
num = 123;
fun(); // undefined
*/
var a = 666;
test();
function test() {
var b = 777;
console.log(a); // 作用域链的就近原则
console.log(b);
console.log(c); // 虽然这里会报错, 但前面该输出的也会输出
var a = 888;
let c = 999;
}
/*
预解析之后:
var a;
function test() {
var b;
var a;
b = 777;
console.log(a); // undefined
console.log(b); // 777
console.log(c); // 报错
a = 888;
let c = 999;
}
a = 666;
test();
*/
- 高级浏览器不会对 {} 中定义的函数进行提升
if(true){
function demo() {
console.log("1");
}
}else{
function demo() {
console.log("2");
}
}
demo(); // 1
/*
纯逻辑: 预解析, 函数提升, 后写覆盖先写, 于是输出 2
实际中:
高级浏览器不会对 {} 中定义的函数进行提升,
于是顺序执行, 输出 1
*/
- 如果同级作用域 var 变量名与函数名同名, 函数的优先级高于变量 (淦! 居然没一以贯之用‘覆盖’的逻辑)
- 一定要记住, 在企业开发中千万不要让变量名称和函数名称重名
console.log(value); // 输出函数的定义
var value = 123;
function value() {
console.log("fn value");
}
console.log(value); // 123
/*
预解析之后
function value() {
console.log("fn value");
}
console.log(value);
var value;
value = 123;
console.log(value);
*/
方法
创建默认对象
- JavaScript 中提供了一个默认的类 Object, 我们可以通过这个类来创建对象
- 由于我们是使用系统默认的类创建的对象, 所以系统不知道我们想要什么属性和行为, 所以我们必须手动的添加我们想要的属性和行为
- 如何给一个对象添加属性 对象名称.属性名称 = 值;
- 如何给一个对象添加行为 对象名称.行为名称 = 函数;
创建对象的三种方式
-
new 一个, 往这个对象里加属性和方法
let obj = new Object(); obj.name = "fhs"; obj.age = 23; obj.say = function () { console.log("hello world"); } console.log(obj.name); console.log(obj.age); obj.say(); -
把个不完整的对象(甚至空对象也可)赋给变量
let obj = {}; // let obj = new Object(); 的简写 obj.name = "fhs"; obj.age = 23; obj.say = function () { console.log("hello world"); } console.log(obj.name); console.log(obj.age); obj.say(); -
直接把完整的对象赋给变量 (格式上有区别的哈, 这里是键值对的键和值以冒号隔开, 且键值对间以逗号隔开)
let obj = { name: "fhs", age: 23, say: function () { console.log("hello world"); } }; console.log(obj.name); console.log(obj.age); obj.say();
注意点:
对象内方法只应该是函数赋给变量的形式, 以正统函数形式存在于对象内的函数不是对象的方法
函数与方法的区别
对象的行为称为方法而非函数
-
什么是函数? 函数就是没有和其它的类显示的绑定在一起的, 我们就称之为函数
-
什么是方法? 方法就是显示的和其它的类绑定在一起的, 我们就称之为方法
-
函数和方法的区别
- 函数可以直接调用, 但是方法不能直接调用, 只能通过对象来调用
- 函数内部的 this 输出的是 window, 方法内部的this输出的是当前调用的那个对象
-
无论是函数还是方法, 内部都有一个叫做 this 的东西
this 是什么? 谁调用了当前的函数或者方法, 当前的 this 就是谁
工厂函数, 构造函数
我悟了, js 中构造函数的名字就是 js 的所谓类名
工厂函数
什么是工厂函数? 工厂函数就是专门用于创建对象的函数, 我们就称之为工厂函数
作用: 降低代码冗余度
/*
let obj = {
name: "zs",
age: 23,
say: function () {
console.log("hello world");
}
};
let obj = {
name: "xhx",
age: 22,
say: function () {
console.log("hello world");
}
};
*/
function createPerson(myName, myAge) {
let obj = new Object();
obj.name = myName;
obj.age = myAge;
obj.say = function () {
console.log("hello world");
}
return obj;
}
let obj1 = createPerson("zs", 23);
let obj2 = createPerson("xhx", 22);
console.log(obj1);
console.log(obj2);
构造函数
朴素版构造函数
- 什么是构造函数
- 构造函数和工厂函数一样, 都专门用于创建对象
- 构造函数本质上是工厂函数的简写 (还限制了创建方式, 更专业)
- 构造函数和工厂函数的区别
- 构造函数的函数名称首字母必须大写
- 构造函数只能够通过 new 来调用
function Person(myName, myAge) {
// let obj = new Object(); // 系统自动添加的
// let this = obj; // 系统自动添加的
this.name = myName;
this.age = myAge;
this.say = function () {
console.log("hello world");
}
// return this; // 系统自动添加的
}
let obj1 = new Person("zs", 23);
let obj2 = new Person("xhx", 22);
console.log(obj1);
console.log(obj2);
当我们 new Person("zs", 23); 系统做了什么?
- 会在构造函数中自动创建一个对象
- 会自动将刚才创建的对象赋值给 this
- 会在构造函数的最后自动添加 return this;
- 注意到, 如果手动补齐构造函数中 js 自动为我们添加的部分, 那就成为了工厂函数
优化构造函数
为什么优化
- 朴素版构造函数两个对象中的 say 方法的实现都是一样的, 但是保存到了不同的存储空间中, 这样性能不好
- 要优化以提升性能
第一版优化
当前这种方式存在的弊端
- 阅读性降低了
- 污染了全局的命名空间
function mySay() {
console.log("hello world");
}
function Person(myName, myAge) {
this.name = myName;
this.age = myAge;
this.say = mySay;
}
let obj1 = new Person("zs", 23);
let obj2 = new Person("xhx", 22);
// 通过三等来判断两个函数名称, 表示判断两个函数是否都存储在同一块内存中
console.log(obj1.say === obj2.say); // true
第二版优化
fns 是 function name space 的缩写, hhh 这个相比第一版并没有进步很多嘛
// function mySay() {
// console.log("hello world");
// }
let fns = {
mySay: function () {
console.log("hello world");
}
}
function Person(myName, myAge) {
this.name = myName;
this.age = myAge;
this.say = fns.mySay;
}
let obj1 = new Person("zs", 23);
let obj2 = new Person("xhx", 22);
console.log(obj1.say === obj2.say); // true
三版优化 (原型入门)
prototype 是对象, 在这里 prototype 是构造函数这个对象的对象
用原型做到了真正不污染, 但可读性方面存在一定门槛
我在 chrome 尝试了一下, 构造函数本质和普通函数还真是一样的
// let fns = {
// mySay: function () {
// console.log("hello world");
// }
// }
function Person(myName, myAge) {
this.name = myName;
this.age = myAge;
}
Person.prototype = { // 用 Person.prototype.say = function (){} 也能行, 亲测能行
say: function () {
console.log("hello world");
}
}
let obj1 = new Person("zs", 23);
let obj2 = new Person("xhx", 22);
console.log(obj1.say === obj2.say); // true
补充: 第十行这种被称为自定义原型对象, 第十行注释部分称为给原型对象动态添加方法