学编程,来源栈;先学习,再交钱
当前系列: 编程语言 修改讲义


承上启下:代码要写得好

其实你学完了变量赋值、分支循环,理论上你就学“完”编程了:任何功能,本质你都可以用分支循环直接实现。

接下来,其实我们要学的,是如何把代码写“好”。怎么才算好?

一个例子:老司机 vs 新手上路

只要拿到驾照,其实大家的技术(油门/刹车/离合/方向盘)都一样的,但是:

  • 新手:慢慢腾腾,这里熄个火那里刮一下,遇到个极端情况,车子摆在路中间不知道咋整……
  • 老司机:稳准狠,任何路况,保证通过,及时到达……

@想一想@:他们的差距在哪里?

有一种很流行的说法:要懂“底层”,老司机车开得好,是因为他更懂发动机?

代码以外

  1. 需求,需求,还是需求:重要的事说三遍!已经讲过:模糊需求,后面还讲:需求的变更,illogical Business Logic
  2. 进度:工作量预估,一般都是往上加buffer
  3. 其他:沟通、表达、协调、管理……颈椎病预防啥的

代码以内

安全、性能、可维护性。

专讲可维护性(代码会一直用反复改,和“建筑工程”其实不同):

  • 可读性:变量命名、注释、条理性……看上去最简单,实际上很难
  • 健壮性:
    1. 一开始就bug要少(没有bug是不可能的,这辈子都不可能):比如:已经讲过的else兜底;还没讲的:TDD、防御式编程
    2. 经得起“折腾”:拥抱变化(高级货,面向对象涉及更多)


为什么需要函数?

函数(function),又被称之为“子程序”,面向对象时称之为方法(method),几乎所有的高级语言中都有这个概念。

其本质就是将一段代码予以封装,以便于以后反复使用(重用)。

比如我们之前的“从数组中查找某个”,这一功能就可以在项目中反复使用:在任意一个数组中查找任意一个值,这就可以被封装成一个函数。

@想一想@:不然怎么办?

记住:程序员讨厌复制粘贴!

演示:复制粘贴的三个问题:

  1. 增加代码量
  2. 修改的过程中引入bug
  3. 一处修改,四处更改


声明

所谓函数的声明(declare),就是函数的创建 (复习:变量的声明

任何函数,一定包含:

  • 参数(parameter/argument):在哪个数组中,找哪一个数值
  • 函数体:具体怎么找?
  • 返回值(return):查找的结果


JavaScript示例

最常规声明方式:

function find(numbers, target) { 
	return -1;   //或者其他值
}

语法点:

  • function:关键字,(语法上)必须的,表明这是一个函数
  • find:函数名,(语法上)必须的,由开发人员自定,规则同变量
  • (numbers, target):圆括号是必须的,里面放置参数。参数可以没有、有1个或者多个,多个参数间用逗号(,)分隔,比如(numbers, target)。参数名由开发人员自定,规则同变量名
  • {}:花括号是必须的,里面放置一段代码,被称之为“函数体”,可以更复杂,详见后文。
  • return:关键字,只能在函数体中,指定函数的返回值。return之后的语句不会再执行。如果函数体中没有return语句,或者return后未接表达式,返回undefined。


调用

函数声明之后,不会自动运行。函数运行,需要开发人员调用(使用,call/invoke):

调用函数的代码被称之为函数表达式

find([2, 5, 8, 1, 7, 12, 9],  8);

函数表达式执行完成之后就“代表”函数返回值,可以赋值给变量,也可以直接使用

let result = find([2, 5, 8, 1, 7, 12, 9],  8);
alert(find([1, 9, 7, 3, 15, 5, 4, 6], 3))

通过调用:(F12断点演示)

  1. 我们向函数传递了参数,
  2. 函数开始运行,能够使用上述参数
  3. return之后的语句不会被执行
  4. 运行结束之后,返回调用函数处,我们还能拿到函数的返回值

示例,声明和调用:

  • 没有参数的函数
    function find() { 
    	return 986;   //或者其他值
    }
  • 没有返回值的函数
    function find() { 
    	console.log("源栈欢迎你!");
    }
    注意:console.log()控制台输入≠ return
  • 真正的封装:
    function find(numbers, target) { 
    	for (let i = 0; i < numbers.length; i++ ){
    		if(numbers[i]==target){
    			//break;
    			return i+1;
    			console.log("找到了,在第"+ (i+1) + "位");	
    		}
    		else{
    			if(i == numbers.length-1){
    				console.log("没找到");	
    				return -1;
    			}//else nothing		
    		}
    	}
    }

防御式和截断式

有些同学会好奇无返回值return;的意义,简单的说,它可以和if...else配合,在某些条件下终止函数的执行。

比较常见的情景包括:

  • 防御式编程:在函数主体代码执行之前首先检查传入的参数是否适合该函数,比如:
    function isPositive(number){
    	//函数的本义是检查一个数字是否为正数,
    	//所以如果传入的number都不是数字的话
    	if(isNaN(number)){
    		console.log("无法判断一个非数字的值是否为正数");
    		return;
    	}
    	return number > 0;
    }
  • 截断式编程:将一个复杂问题中较为简单的部分剥离出来,优先处理:
    function doSomething(number){
    	//要根据用户角色进行各种不同的复杂的处理
    	//但是,如果用户根本就没登录……
    	if(role==null){
    		console.log("未登录用户请先登录/注册");
    		return;	
    	}
    	
    	//其他代码....
    }
    这样剩下的问题也会逐渐变得简单。

无论是防御式,还是截断式,都能减少else的使用,从而减少分支的嵌套层级。

if 中直接返回了,不需要在额外加上else。

break和return

  • break:跳出循环,循环后(外)面的代码还是要执行
  • return:跳出函数,所有的代码都不执行了

形参 vs 实参

PS:不重要,愿意理解的就了解一下

  • 形参:形式参数,在函数定义的时候指定的参数。形参的参数名是开发人员自己取的,只作用于函数内部。
  • 实参:实质参数,在调用函数的时候实实在在传递的参数/变量/值。

最后,alert()和console.log()都是函数。

native code:非JavaScript实现,看不到内容


常见错误

混淆“输出”和“返回”:console.log()是返回是返回是返回!

当方法体中出现分支循环时,以为有“多个”返回



演示:短路运算

断点调试时:

  • F11:进入函数内部
  • Shift+F11:跳出当前函数
准备两个函数:
function isPositive(number){
	return number > 0;
}
function isOdd(number){
	return number % 2 == 1;
}

代码:

if(isPositive(-5) && isOdd(20)){
  • 短路运算:一旦得到运算结果,就不再进行后面的运算
  • 非短路运算:(|)和(&),不推荐……


递归

在方法体中还可以调用方法自己,这被称之为递归(调用)。

通常我们并不鼓励采用这种写法,因为它占用“栈”资源,导致运行效率低,甚至会造成堆栈溢出(Stack Overflow)。

但有时候,使用递归能够大幅精简代码书写,比如:二分查找,或者查找一个文件夹下所有的文件等,这里我们讲一个经典的:

斐波那契数列

0,1,1,2,3,5,8,13,21,34,55,89,144……依次类推下去。

你会发现,它后一个数等于前面两个数的和。在这个数列中的数字,就被称为斐波那契数。

思路:

  1. 第一次,传入0和1,得到第三个数字1(=0+1)
  2. 但同时,要把本次调用中的参数1,和生成的第3个数字1作为参数再传入当前函数
  3. ……
  4. 但一定要设置好终止条件!
    function getFibonacci(a, b)
    {
        //终止条件
        if (b > 1000)
        {
            return;
        }

        console.log(b);
        let sum = a + b;
        //递归的调用自己
        getFibonacci(b, sum);
    }


重用一时爽

一直重用一直爽!

同学们要更深刻的理解函数的价值:封装和重用。不仅仅是封装一段代码,函数和函数之间也可以形成封装和重用。

比如最初,我们有这么一个函数:

    //返回数组中的第一个元素
    function getFirst(numbers){
	return numbers[0];
    }

后来,我们又有了这么个函数:

    //返回数组中的最后元素
    function getLast(numbers){
	return numbers[numbers.length-1];
    }
最后,我们进一步的拓展,有了这么一个函数:
    //返回由参数指定位置/下标的数组中某元素
    function getBy(numbers, index){
	return numbers[index];
    }

@想一想@:有啥不对劲的地方不?

getFirst()和getLast()是不是能够重用一下getBy()呀?

    function getFirst(numbers){
	return getBy(numbers, 0);
    }

这有没有价值?^_^


作业

  1. 声明一个函数:函数名为add,接受两个参数a和b,调用该函数,能返回两个参数的和。
  2. 声明函数welcome(sname),能弹出窗口,显示包含学生姓名(sname)的欢迎词,比如:“源栈欢迎你!阿泰”(sname=阿泰)
  3. 将之前“找出素数”的代码封装成一个函数findPrime(max),可以控制台输出max以内的所有素数。
  4. 声明以下函数,接受一个数组作为参数:
    1. getSum:返回数组中所有元素的和
    2. getAverage:返回数组中所有元素的平均值
    3. getMax:返回数组中最大的元素
    4. getMin:返回数组中最小的元素
    5. getRange:数组中最大一个元素和最小元素的差
    @想一想@:以上函数间能够相互调用,形成重用么?
  5. 设计一个函数sum,能(用遍历而不是公式)求出任意等差数列(如:1,3,5……或者0,5,10,15……)之和。(提示:等差数列可由
    • 起始位置(start)
    • 相邻两元素差值(step)
    • 元素个数
    确定)
  6. 设计一个函数swap,能够交换数组中的任意两个元素(提示:引入中间变量)

学习笔记
源栈学历
键盘敲烂,月薪过万作业不做,等于没学

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 编程语言 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码