高内聚低耦合->模块的单一责任制->强的功能性和独立性
解耦合-> 将重复的代码功能抽离成单独的函数。
函数可以解耦合
定义函数
定义函数有两种:
一种是函数声明,
另一种就是函数表达式
函数声明
函数声明包括把函数体赋值给函数名称这一步骤
function functionName(arg0,arg1){
//函数体
}
//浏览器都给函数定义了一个非标准的name属性
//通过这个属性可以访问到函数的名字
//这个属性的值永远等于function关键字后面的标识符
console.log(functionName.name)//"functionName"
- 关于函数声明,它最重要的特征就是函数声明提升,
- 在执行函数之前会先读取函数声明。
- 这意味着可以把函数声明放在调用它的语句后面
console.log(sayHi.name,sayHi.length,sayHi.toString())
// sayHi 0 function sayHi(){
// alert('hi')
// }
sayHi();
function sayHi(){
alert('hi')
}
禁止使用 var a = b = 1;
声明和初始化变量
function test(){
var a = b = 1;
console.log(a,b) //1 1
}
console.log(b) // 1
console.log(a) // a not defined
构造函数式
//最后一个参数表示函数体,前面的参数表示函数的参数
var demo3 = new Function('a','b','console.log(a+b)')
demo3(1,2)//3
//设置返回值
var demo3 = new Function('a','b','return a+b')
var result = demo2(1,3)
- 让字符串作为语句执行的第二种方法 eval
eval('console.log(1+2)')
-
区别
他们都可以执行字符串函数
eval("var e = 'e'")//eval中定义的变量e为全局变量
var demo4 = new Function('a','b','var a = 0;return a+b')
demo4(1,3)//new Function中定义的变量是函数中的变量为局部变量
- 字符串和数字当做工厂方法去使用,不是创建对象实例,而是包装对象(类型的转换)
var str = new String(100)
var str2 = String(100)
console.log(str,str2)
var num1 = new Number(100)
var num2 = Number(100)
console.log(num1,num2)
//工作中经常使用String与Number工厂方法实现显性的数据类型转换
var b1 = new Boolean(100)
var b2 = Boolean(100)//包装 显性转换
console.log(b1,b2)
//创建正则
var reg1 = /^a/ //正则字面量
var reg2 = new RegExp('^a','i')//参数1 正则内容, 参数2 正则修饰符
//工厂方法和构造函数是一样的
var reg3 = RegExp('^a','i')
console.log(reg2,reg3)
var err = new Error('这是一个错误')
console.log(err)
console.log('abc')
// Error也是一个安全类
console.log(Error('这是一个错误'))
//抛出错误 红色字体 程序会被终止
throw err
//日期 安全类
var d = new Date()
var d2 = Date()
console.log(d,d2)
函数表达式
命名函数表达式
将一个函数赋值给一个变量
//有标识符的叫命名函数表达式
var functionName = function foo(arg0, arg1, arg2) {
//函数体
};
functionName.name == "foo"
//foo 只能在函数体内访问
eg
// test()
var test = function testFun(){
var a = 1,b = 2;
console.log(a,b);
console.log(testFun.name,1) //testFun 1
console.log(test.name,2) //testFun 2
}
console.log(test.name);//test
test() //1 2
console.log(testFun) //testFun is not defined
匿名函数表达式
去掉function后面的函数名称,也叫函数字面量表达式
函数表达式有几种不同的语法形式,最常见的如下
var functionName = function(arg0,arg1){
//函数体
}
这种形式看起来像是最常规的变量赋值语句,
即创建一个函数并将它赋值给变量functionName.
这种情况下创建的函数叫匿名函数,也叫拉姆达函数。
匿名函数的name属性是空字符串
函数表达式与其他表达式一样,在使用前必须赋值
函数表达式无论是否有名字都没有函数提升
sasyHi()//报错
sayHi(){
alert('hi')
}
函数声明与函数表达式之间的区别
理解函数提升的关键就是理解函数声明与函数表达式之间的区别
//不要这样做
if(condition){
function sayHi(){
alert('hi')
}
}else{
function sayHi(){
alert('Yo')
}
}
表面上看,以上代码在condition=true时,使用sayHi()的定义;否则就使用另一个定义。
实际上在ES中这是一个无效语法。JS引擎会尝试修正错误,将其转换为合理的状态。
但每个浏览器的JS引擎修正错误的做法都不一致。
大多数浏览器都返回第二个声明,忽略condition。
不过如果使用函数表达式那就没有什么问题了
var sayHi
if(condition){
sayHi = function(){
alert('hi')
}
}else{
sayHi = function(){
alert('Yo')
}
}
函数的参数和返回值
arguments、functionName.length
//将一个函数字面量赋值给test->匿名函数表达式
var test = function(){
console.log(1)
}
//==========================================
//函数声明
function test(a,b){
console.log(a,b)
console.log(test.length)//查看形参的个数
console.log(arguments) //查看调用函数的实参
}
var a= Number(window.prompt('a'))
var b= Number(window.prompt('b'))
test(a,b) //实参和形参的调用
实参和形参的区别和求和问题
- 函数中使用
arguments
查看调用函数的实参 - 函数中使用
functionName.length
查看形参的个数 - 实参和形参数量可以不相等
- 实参比形参少,未被赋值的形参在函数中为undefined
- 实参比实参多,没有影响
- 实参arguments求和(面试题)
//实参求和:一个函数被调用时累加它的实参值
function sum(){
var a=0;
for(var i=0;i<arguments.length;i++){
a+=arguments[i]
}
console.log(a)
}
sum(1,2,3)
实参和形参的关系
必记
- 如果有传入对应形参,则函数内部可以通过改变它形参并且影响他的实参arguments的值,改变对应实参其对应形参也会改变,此时他们是一一映射的,但存在内存中不同的位置
- 如果没有传入对应形参,函数内部对应的实参为undefined,此时函数内部改变形参不影响实参arguments的值,改变对应实参其对应形参也不会改变,此时没有一一映射的关系
function test(a,b){
a=3;
b=6
console.log(arguments[0])//3 函数可以改变实参的值
console.log(arguments[1])//undefined
arguments[0] = 9;
arguments[1] = 9
console.log(a,arguments[0]) //9 9
console.log(b,arguments[1]) //6 9
}
test(1)
arguments.callee、递归、命名函数表达式的应用
arguments.callee 属性包含当前正在执行的函数。
callee 是 arguments 对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。
这在函数的名称是未知时很有用,例如在没有名称的函数表达式 (也称为“匿名函数”)内。
//==========================
//this 函数内部的this
/**
全局this->window
预编译函数this->window
apply/call函数改变this指向
构造函数的this指向实例化对象
**/
function test(a,b,c){
console.log(arguments.callee.length)//3
console.log(test.length)//形参的个数
//arguments.callee 是当前正在执行的函数实例
//arguments.callee.length = test.length
console.log(arguments.length)//实参的个数
}
test(1,2)
- 递归和阶乘
递归函数是在一个函数中通过名字调用自身的情况下构成的
-
计算阶乘(数字太大会有性能问题)
function fact(n){ if(n<=1){ return 1 } return n * fact(n-1) } console.log(fact(5))
这是一个经典的递归阶乘函数。
虽然这个函数表面上看起来没有什么问题,但下面的代码却可能导致它出错function fact(n){ if(n<=1){ return 1 } return n * fact(n-1) } //先把fact()递归函数保存在变量anotherFact中 var anotherFact = fact //然后将fact变量设置为null fact = null //结果指向原始递归函数的引用只剩下anotherFact //但在函数内部返回时又需要调用fact() //而fact已经是null了所以会报错 fact(5) //在这种情况下使用arguments.callee 可以解决这个问题 //arguments.callee是一个指向当前正在执行函数的指针,因此可以用它来实现对函数的递归调用 function fact(n){ if(n<=1){ return 1 } return n * arguments.callee(n-1) }
-
和的累加
function sum(n){ if(n<=1){ return 1 } return n +sum(n-1) } var res = sum(3)//6 //如果是匿名函数,如何递归 。 //-->当前函数arguments.callee var sum = (function(n){ if(n<=1){ return 1 } return n +arguments.callee(n-1) })(3)
但在严格模式下,不能通过脚本访问arguments.callee,访问这个属性会导致错误,
不过可以使用命名函数表达式达成相同的结果
var fact = (function f(n){
if(n<=1){
return 1
}
return n * f(n-1)
})
命名函数表达式的另一种写法
var fact = function f(n){
if(n<=1){
return 1
}
return n * f(n-1)
//f只在函数内部可见,外部不可用
}
fact.name //"f"
上面代码创建了一个为f的命名函数,然后将它赋值给变量fact。
即使把函数赋值给了另一个变量,函数的名字f任然有效。所以递归照样能正确调用。
警告:在严格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。当一个函数必须调用自身的时候, 避免使用 arguments.callee(), 通过要么给函数表达式一个名字,要么使用一个函数声明.
- 为什么 arguments.callee 从ES5严格模式中删除了?
- 早期版本的 JavaScript不允许使用命名函数表达式,只能使用匿名函数表达式,
出于这样的原因, 你不能创建一个递归函数表达式。 - 为了解决这个问题, arguments.callee 添加进来了。
- arguments.callee的缺点:
要原因是递归调用会获取到一个不同的 this 值,例如:var global = this; var sillyFunction = function (recursed) { if (!recursed) { return arguments.callee(true); } if (this !== global) { alert("This is: " + this); } else { alert("This is the global"); } } sillyFunction();
- ECMAScript 3 通过允许命名函数表达式解决这些问题。例如:
[1,2,3,4,5].map(function factorial (n) { return !(n > 1) ? 1 : factorial(n-1)*n; });
- 早期版本的 JavaScript不允许使用命名函数表达式,只能使用匿名函数表达式,
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/arguments/callee
caller
- 返回调用指定函数的函数.
该特性是非标准的,请尽量不要在生产环境中使用它
如果一个函数f是在全局作用域内被调用的,则f.caller为null,相反,如果一个函数是在另外一个函数作用域内被调用的,则f.caller指向调用它的那个函数.
该属性的常用形式arguments.callee.caller替代了被废弃的 arguments.caller.
- 例子: 检测一个函数的caller属性的值
下例用来得出一个函数是被谁调用的.
function myFunc() {
if (myFunc.caller == null) {
return ("该函数在全局作用域内被调用!");
} else
return ("调用我的是函数是" + myFunc.caller);
}
ES6对arguments的弱化
ES6中对arguments的弱化
- 函数形参中但凡有一个参数有默认值,arguments和实参没有映射关系
- 函数形参中如果使用rest参数,也没有映射关系
- 函数形参中如果使用参数结构的形式,也没有映射关系
- 严格模式下,也没有映射关系
- arguments不是箭头函数中的内置变量
function test(a = 100){
arguments[0] = 10;
console.log(a,arguments[0]);//1 10
}
test(1)
function test2(a = 100){
a = 1000;
console.log(a,arguments[0]);//1000 1
}
test2(1)
- arguments不是箭头函数中的内置变量
arguments是类数组,我们在里面定义了一些caller,callee等方法
现在caller,callee等方法已经不被需要了
而且开发中常常需要将arguments转换为数组,调用数组的默认方法。
所以ES6中的arguments正慢慢弱化其使用
var test = (...args)=>{
// console.log(arguments)
console.log(args);
console.log(Array.isArray(args))
}
-
arguments性能杀手
-
最佳实践应该使用es6的语法rest参数来替代arguments
-
怎么样安全的使用 arguments ?
- arguments.length
- arguments[i] 这里 i 必须一直是 arguments 的整数索引, 并且不能超出边界
- 除了 .length 和 [i], 永远不要直接使用 arguments
- 严格地说 x.apply(y, arguments) 是可以的, 但其他的都不行, 比如 .slice. Function#apply 比较特殊
function test(){
// slice 用在arguments上会阻止js引擎做一些特定的优化
var argArr = [].slice.call(arguments);
console.log(argArr);
}
解决:
// 处理方法则是使用内联的代码创建数组:
function doesntLeakArguments() {
//.length 只是一个整数,不会泄露
// arguments 对象本身
var args = new Array(arguments.length);
for(var i = 0; i < args.length; ++i) {
//i 始终是 arguments 对象的有效索引
args[i] = arguments[i];
}
return args;
}
function anotherNotLeakingExample() {
var i = arguments.length;
var args = [];
while (i--) args[i] = arguments[i];
return args
}
function anotherNotLeakingExample2() {
var args = arguments.length ==1 ? arguments[0]
: Array.apply(null,arguments);
return args
}
参考 JavaScript 性能优化杀手、
MDN、
managing-arguments、
原因stackoverflow
函数中的return和表单验证
function test(){
console.log('我正在执行')
console.log('我执行完了结束这个函数')
//每个函数都会默认在最后添加一条return用于结束函数
//return 也可以返回一个任何类型的数据 return 'hello'
// return 'hello'
}
console.log(test() )
//表单验证
function test(name){
if(!name){
return '您没有填写姓名!'
}
return name
}
function test(name){
return name || '您没有填写姓名!'
//undefined null NaN 0 空字符串 都是false
}
参数默认值的问题
- 当传入的实参是undefined,而形参默认值不是undefined时,形参生效
- ES5不支持形参上写默认值,ES6才开始支持
function test(a=1,b){
console.log(a)
console.log(b)
}
//如何只给b传入实参,让a为默认值
//给a传入undefined
test(undefined,2) //1,2
//---------------------
function test(a=undefined,b){
console.log(a)
console.log(b)
}
test(1,2) //1,2
//----------------------
//实参 和 形参的映射关系
//实参和形参是一一对应,且是放在不同位置的两个内存空间
//当传入的实参是undefined,而形参默认值不是undefined时,形参生效
//ES5不支持形参上写默认值,ES6才开始支持
- 参数默认值的最佳实践
- 常见最佳写法
function test(a,b){
var a = arguments[0] || 1
var b = arguments[1] || 2
console.log(a)
console.log(b)
}
//如何只给b传入实参,让a为默认值
//给a传入undefined
test(undefined,2) //1,2
- 另一种写法
function test(a,b){
var a,b
if(typeof(arguments[0]) === 'undefined'){
a=1
}else{
a = arguments[0]
}
if(typeof(arguments[1]) === 'undefined'){
b=2
}else{
b = arguments[1]
}
//也可以使用三目运算
console.log(a)
console.log(b)
}
函数参数调用和call、apply、bind
function test(param){
console.log("test method",param)
}
var obj ={}
//直接调用
test('hi')
//通过其他对象调用
//obj.test() 不能直接调用
//可以让一个函数成为指定任意对象的方法进行调用
test.call(obj,'hi')
test.apply(obj,['hi'])
test.bind(obj)('hi')
//new调用
new test()
- call()、apply()、bind() 都是用来重定义 this 这个对象的!他们都是Function的原型方法
- 以上除了了 bind 方法后面多了个 () 外 ,结果返回都一致!
由此得出结论,bind 返回的是一个新的函数,你必须调用它才会被执行。 - call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,第二个参数差别就来了:
- call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面
obj.myFun.call(db,'param1', ... ,'string' )
。 - apply 的所有参数都必须放在一个数组里面传进去
obj.myFun.apply(db,['param1', ..., 'string' ])
。 - bind 除了返回是函数以外,它的参数和 call 一样。
- 当然,三者的参数不限定是 string 类型,允许是各种类型,包括函数 、 object 等等!
- call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面
- 调用函数时传递变量时,是值传递还是引用传递?
- 理解1
都是值传递(基本、地址值) - 理解2
可能是值传递,也可能是引用传递
函数中的this
任何函数本质上都是通过对象来调用的,如果没有显式指定则这个对象是window
所有函数内部都有一个变量this,
它的值是调用函数的当前对象
test() this 指window
new test() this 指当前新创建的对象
//全局变量
b=2
function test1(){
//局部变量
a=1
console.log(b)
function test2(){
//局部变量
var c = 3
console.log(b)
}
test2()
console.log(c)
}
test1()
console.log(a) //a is not defined
console.log(typeof a) //"undefined"
console.log(typeof(a)) //"undefined"
JS引擎预编译流程
<html>
<body>
<script>
console.log(1)
console.log(a)
</script>
</body>
</html>
直接打开页面报错a is not defined
,没有打印1,这是js预编译时报的错误
- 预编译探索
console.log(a) //undefined
var a =1 //如果不写var a, 报错 is not defined
// 总结:var变量的声明提升
//全局变量的隐式声明(不写var 默认为全局变量)
//全局对象都是window的属性
var a =1//=> window.a=1 => a=1
console.log(a)
function test(){
var a= b =1
// b 前面没有var 会被提升到全局变量,所有函数外部可以访问b,则a没有提升,还是在函数内部
}
test()
console.log(a) //a 不能访问 报错 is not defined
console.log(window.a) // 没报错 undefined
//直接访问对象里面不存在的属性为undefined
//直接访问未定义的变量则报错is not defined
console.log(b) //1
AO(activation object)活跃对象->函数上下文
AO = {
1. 形参变量声明和函数体内变量声明
2. 实参赋值给形参
3. 寻找函数声明赋值为函数体
4. 执行此函数体内代码
}
//函数声明
function test(a){
console.log(a) //ƒ a(){}
var a =1
console.log(a) //1
function a(){}
console.log(a) //1
var b = function(){}
console.log(b) //ƒ (){} 匿名函数
function d(){}
}
test(2)
//当test(2)执行时
//函数上下文 创建AO活跃对象(activation object)
/**
AO = {
1. a:undefined,(形参变量声明)
b:undefined,(函数体内变量声明, var b = fun…)
2. a=>2(实参赋值给形参)
3. a=>fun… ,d=>fun…(寻找函数声明赋值为函数体)
4. 执行此函数体内代码
}
**/
function test(a,b){
console.log(a)//1
c=0
var c
a=5
b=6
console.log(b)//6
function b(){}
function d(){}
console.log(b) //6
}
test(1)
/**
AO = {
1. a,b:undefined,(形参变量声明)
c:undefined,(函数体内变量声明)
2. a=>1(实参赋值给形参)b=>undefined
3. b=>fun… ,d=>fun…(寻找函数声明赋值为函数体)
4. 执行此函数体内代码
}
**/
- AO.3是函数声明提升,所以它优先级比较高会覆盖var同名变量提升
- AO.2实参赋值给形参,也包含参数默认值的处理
function test(a){
console.log(a) //ƒ a(){}
var a =1
console.log(a) //1
function a(){}
}
test(2)
// =========
function test(a = 3){
console.log(a) //3
var a =1
console.log(a) //1
}
test()
GO(global object)全局上下文
js中,GO===window
GO = {
1. 找变量但不赋值
2. 找函数声明赋值为函数体
3. 执行代码
}
- 在函数体中如果直接写
c=1
,那么c会被提升到GO的变量中 - 预编译不会执行if语句,所以if语句块中定义的变量也会被提升
var a =1
function a(){
console.log(2)
}
console.log(a) //1
//GO global object 全局上下文
/**
GO===window
GO = {
1. 找变量但不赋值
2. 找函数声明赋值为函数体
3. 执行代码
}
**/
function test(){
var a=b=1;
console.log(b)
}
test()// 函数执行后b被挂载到全局变量中
//以上预编译不管是否在if内
if(true){
var a = 3
}else{
var a = 4
}
var a
if(typeof(a)){ //typeof(a) 为 "undefined"字符串,字符串为true
console.log(123)
}
console.log(a)//3
作用域与作用域链
我们以及了解了js引擎的预编译以及AO和GO
作用域和作用域链式保存AO和GO的容器
变量提升和函数提升
- 变量声明提升
通过var定义的变量,在定义语句之前就可以访问到。值为:undefined - 函数声明提升,通过function声明的函数在之前就可以直接调用。值为:函数定义(对象)
- 问题:变量提升和函数提升是怎么产生的
var a = 3
function fn(){
console.log(a) // undefined
var a = 4
}
fn()
console.log(b) //undefined 变量提升
var b = 3
fn2() //可调用 函数提升 正常执行
function fn2(){
console.log('fn2()')
}
/**
var fn3 = function(){
console.log('fn3()')
}
只有通过function声明直接定义的函数才会有函数提升
**/
//全局变量函数都会挂载到window上
IF块作用域的变量提升和函数提升
- var变量:无论if true 还是 false var 变量正常提升
console.log(a,b)// undefined undefined if(false){ var b = 0; } if(true){ var a = 1; } console.log(a,b) //1 undefined
- if内函数提升同var变量,条件式函数声明丧失了函数声明提升的特性。
console.log(a,b)// undefined undefined if(false){ function b(){}; } if(true){ function a(){}; } console.log(a,b) //ƒ a(){} undefined
- 块级作用域下的函数声明=>函数提升同var声明,执行同let 函数赋值
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions#非严格模式下的块级函数console.log(a)//undefined if(true){ function a(){}; a = 1; console.log(a) //1 } console.log(a) //ƒ a(){}
执行上下文、执行上下文栈
- 执行上下文
- 代码分类
- 全局代码
- 函数(局部)代码
- 全局执行上下文
- 在执行全局代码前将window确定为全局执行上下文
- 对全局数据进行预处理
- var定义的全局变量==>undefined,添加为window的属性
- function声明的全局函数==>赋值(fun),添加为window的方法
- this==> 赋值(window)
- 开始执行全局代码
- 函数执行上下文
- 在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象,存在于栈中
- 对局部数据进行预处理
- 形参变量==>赋值(实参)==>添加为执行上下文的属性
- arguments==>赋值(实参列表),添加为执行上下文的属性
- var定义的局部变量==>undefined,添加为执行上下文的属性
- function声明的函数==>赋值(fun),添加为执行上下文的属性
- this==>赋值(调用函数的对象)
- 开始执行函数体代码
//函数执行上下文
function fn(a1){
console.log(a1)//2
console.log(a2)//undefined
a3() //a3
console.log(this) //window
console.log(arguments)//伪数组[2,3]
var a2 =3
function a3(){
console.log('a3()')
}
}
fn(2,3)// 这边通过window调用的函数,所以函数里this为window
- 执行上下文栈
- 在全局代码执行前,js引擎将会创建一个栈来存储管理所有的执行上下文对象GO
- 在全局执行上下文(window)确定后,将其添加到栈中(压栈)
- 在函数执行上下文栈创建后(管理AO),将其添加到栈中(压栈)(此时函数执形上下文在栈顶)
- 在当前函数执行完后,将栈顶的对象移除(出栈)
- 当所有代码执行完后,栈中只剩下window(GO)
作用域
- 分类
- 全局作用域
- 函数作用域
- 没有块作用域(ES6有了)
- 作用:隔离变量,不同作用域下同名变量不会冲突
- 作用域与执行上下文
- 区别1
- 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了,而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定后,js代码马上执行前创建的
- 函数执行上下文环境是在调用函数时,函数体代码执行之前创建的
- 区别2
- 作用域是静态的,只要函数定义好了就一直存在,且不会再变化
- 上下文环境是动态的,调用函数时创建,函数调用结束时上下文环境就会被释放
- 联系
- 上下文环境是从属于所在的作用域
- 全局上下文环境==>全局作用域
- 函数上下文环境==>对应的函数作用域
- 区别1
- 面试:作用域在函数定义时就已经确定了,而不是在函数调用时
var funOuter = function(){
console.log(this.name)
}
var name = 222
var b = {
name:333,
say:function(fun){
console.log(this.name)//333
fun() //这里执行的是funOuter函数
// 对于this,谁调用方法,this指向谁,所以传进来的函数的this没有被改变
}
}
b.say(funOuter) //222
// 为啥是222
// 函数是对象,把函数当做参数传入的是函数的引用,而函数的作用域在定义的时候就已经确定了,不管你在哪儿调用,它的作用域不会改变
- 面试题2
var marty = {
name : 'marty',
printName:function(){
console.log(this.name)
}
}
var test1 ={
name:'test1'
}
var test2 = {
name:'test2'
}
var test3 = {
name:'test3'
}
test3.printName = marty.printName;
marty.printName.call(test1)
marty.printName.apply(test2)
marty.printName()
test3.printName();
// VM405:4 test1
// VM405:4 test2
// VM405:4 marty
// VM405:4 test3
- 面试题3
function Foo(){
// 这里声明的getName是全局变量
getName = function(){
console.log(1)
}
return this;
}
// 相当于对象中的一个属性
Foo.getName = function(){
console.log(2)
}
Foo.prototype.getName = function(){
console.log(3)
}
var getName = function(){
console.log(4)
}
function getName(){
console.log(5)
}
Foo.getName() //2
getName() // 4
Foo().getName() //1
new Foo.getName(); //2 点运算符比new高
new Foo().getName();//3 new Foo()比点运算符高
new new Foo().getName() //3 new Foo().getName()
作用域链
- 理解
- 多个上下级关系的作用域形成的链,他的方向是从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找的
- 变量的查找规则
- 在当前作用域下的执行上下文中查找对应的属性,如果有直接返回,否则进入2
- 在上一级作用域的执行上下文中查找对应的属性,如果有直接返回,否则进入3
- 再次执行2的相同操作,直到全局作用域,如果还找不到就抛出找不到的异常
- 面试题1
var x =10;
function fn(){
console.log(x)
//无论谁调用这个函数,在这个函数中如果找不到x,它向上一级(全局作用域查找)
//作用域是静态的,只要确定好了就不好再变化,不管你怎么调用
}
function show(f){
var x = 20;
f();
}
show(fn) //10
- 面试题2
var fn = function(){
console.log(fn)
}
fn() //输出fn这个函数
var obj = {
fn2:function(){
console.log(fn2) //当前函数作用域没找到fn2 ,向上全局作用域找,找不到报错
}
}
obj.fn2()//Uncaught ReferenceError: fn2 is not defined
var obj = {
fn2:function(){
console.log(this.fn2) //如果要找到内部的fn2,前面应该加this
}
}
obj.fn2()
AO、GO
我们学习AO GO 是为了解决作用域链相关所产生的一切问题
AO=>function 独立内存空间
js对象有些属性是我们无法访问的,这是js引擎内部固有的隐式属性
- [[scope]]
- 函数创建时生成的其函数对象内部的隐式属性[[scope]]
- 当函数被定义时,全局执行上下文生成[[scope]]属性,它保存的是该函数的作用域链,作用域链的第0位存储全局执行上下文GO
- 当函数执行的时候,函数执行上下文生成AO,压入作用域链的最顶端。
- 函数执行结束会销毁AO,AO是一个即时的存储容器,每次调用函数都会产生新的AO
function a(){
function b(){
var b=2
}
var a =1
b()
}
var c=3
a()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IQD17mTP-1652100289770)(https://blog.whaleluo.space/assets/imgs/js/scope1.png)]
每一个函数在定义的时候就生成了GO存放在作用域链
也可以说全局执行的前一刻就生成了GO
每一个函数在执行的前一刻生成了AO
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8DVnXgsK-1652100289772)(https://blog.whaleluo.space/assets/imgs/js/scope4.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tj1GWnUY-1652100289772)(https://blog.whaleluo.space/assets/imgs/js/scope5.png)]
闭包
闭包的定义,优缺点,作用
- 当test2函数被定义的时候,此时它的作用域链和test1的作用域链相同
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4L7N33yp-1652100289775)(https://blog.whaleluo.space/assets/imgs/js/closepack1.png)] - 当test2执行的前一刻生成自己的AO,压入到自己的作用链顶端
1. 当内部函数被返回到外部并保存时一定会产生闭包
2. 闭包会使原来的作用域链不释放
3. 过度的闭包会造成内存泄露或加载过慢
- 闭包可以用作数据缓存
function test(){
var n =100
function add(){
console.log(++n)
}
function reduce(){
console.log(--n)
}
return [add,reduce] //return 以数组的形式返回多个变量
}
var arr = test()
arr[0]()
arr[0]()
arr[1]()
// 闭包可以用作数据缓存
闭包与循环变量
但函数被第一次调用的时候会创建一个执行环境和相应的作用域,
并把作用域压入一个特殊的内部属性[[Scope]]。
然后用this,arguments和其他命名参数的值来初始化函数的活动对象AO(activation object)。
但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位……直至作为作用域链终点的全局执行环境。
由于闭包会携带它的函数的作用域,因此会比其他函数占用更多的内存。
作用域链的这种配置机制引出另一个问题。
即闭包只能取得包含函数中任何变量的最后一个值
//--闭包经典面试题--
function test(){
var arr=[]
for(var i =0;i<10;i++){
arr[i]=function(){
console.log(i+' ')
}
}
return arr
}
//执行arr里面的函数,应该打印出0-9 却打印出10个10
var myArr = test()
for(var j=0;j<myArr.length;j++){
myArr[j]()
}
立即执行函数解决循环中的闭包
//---解析---
function test(){
var arr=[]
var i =0
for(;i<10;){
arr[i]=function(){
console.log(i+' ')
}
}
i++
return arr
}
//---使用立即执行函数解决1---
function test(){
//var arr=[]
for(var i =0;i<10;i++){
(function(){
console.log(i+' ')
}())
}
//return arr
}
//---使用立即执行函数解决2---
function test(){
var arr=[]
for(var i =0;i<10;i++){
(function(j){
arr[j]=function(){
console.log(j+' ')
}
})(i)
}
return arr
}
var myArr = test()
for(var j=0;j<myArr.length;j++){
myArr[j]()
}
//闭包面试题2
//需求: 点击某个按钮 提示“点击的是第n个按钮”
var btns = document.getElementsByTagName('button')
//遍历加监听
for(var i=0;i<btns.length;i++>){
//btns.length btns是一个伪数组,每次执行btns.length都会计算一次,不是一个固定的值
var obj= btns[i]
}
//解决以上问题,提高性能
for(var i=0,length=btns.length;i<length;i++>){
var btn= btns[i]
btn.onclick = function(){
alert('第'+(i+1))
}
}
console.log(i)
//问题 循环中只出现了一个i。i是全局变量。所以 alert('第'+(i+1))总是同样的值
//解决 将btn所对应的下标保存在btn上
for(var i=0,length=btns.length;i<length;i++>){
var btn= btns[i]
btn.index = i
btn.onclick = function(){
alert('第'+(this.index+1))
}
}
//另一种写法 利用立即执行函数来实现
for(var i=0,length=btns.length;i<length;i++>){
(function(i){
var btn= btns[i]
btn.onclick = function(){
alert('第'+(i+1))
}
})(i)
}
闭包总结
-
如何产生闭包?
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时就产生了闭包 -
闭包到底是什么?
- 使用Chrome调试查看 closure
- 理解一:闭包是嵌套的内部函数
- 理解二:包含被引用变量(函数)的对象
- 注意:闭包存在于嵌套的内部函数中
-
产生闭包的条件?
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
-
常见的闭包
- 将函数作为另一个函数的返回值
- 将函数作为实参传递给另一个函数使用
//将函数作为另一个函数的返回值
function fn1(){
//执行到此时闭包就已经产生了(函数提升,内部函数对象已经创建了)
var c= 3
c++
console.log(c + 'in out inner fun')
var a = 2
function fn2(){
a++
console.log(a)
console.log(c + 'is used in inner fun')
//这里的c永远都是4,说明 fn2外面的语句永远都只执行一次,变量c不会销毁
}
return fn2
}
var f = fn1()
//重要
//把fn1(实际上是返回的fn2的地址值)赋值给f,这样函数执行完后,fn2因为有被引用所以它不会销毁
//如果直接执行fn1(),那样它永远都是3,因为没有任何引用,执行完就被销毁了
f()//3
f()//4
//f=null // 闭包死亡(包含闭包的函数对象成为垃圾对象)
//将函数作为实参传递给另一个函数使用
function showDelay(msg,time){
setTimeout(function(){
alert(msg)
},time)
}
showDelay('a',2000)
- 闭包的作用
- 使用函数内部的变量在函数执行完后,仍然存活在内存中(延长了局部变量的生命周期)
- 让函数外部可以操作到函数内部的数据
问题:
- 函数执行完后,函数内部声明的局部变量是否还存在?闭包环境下才存在
- 在函数外部能直接访问函数内部的局部变量吗? 不能,但是通过闭包可以操作
-
闭包的生命周期
- 产生:在嵌套的内部函数定义执行完就产生了(不是在调用)
- 死亡:在嵌套的内部函数成为垃圾对象时
-
闭包的应用;定义js模块
具有特定功能并向外暴露特定方法function myModule(){ //私有数据 var msg = 'Hello' //操作数据的函数 function doSomething(){ console.log('doSomething() '+ msg.toUpperCase()) } function doOtherthing(){ console.log('doOtherthing() '+ msg.toLowerCase()) } //向外暴露 return {doSomething,doOtherthing} }
使用立即执行函数的写法,更方便
(function(){ //私有数据 var msg = 'Hello' //操作数据的函数 function doSomething(){ console.log('doSomething() '+ msg.toUpperCase()) } function doOtherthing(){ console.log('doOtherthing() '+ msg.toLowerCase()) } window.myMoudle2 = {doSomething,doOtherthing} })()
下面的这种写法便于代码压缩,代码中所有的window可以被压缩为w单个字符
(function(window){ //私有数据 var msg = 'Hello' //操作数据的函数 function doSomething(){ console.log('doSomething() '+ msg.toUpperCase()) } function doOtherthing(){ console.log('doOtherthing() '+ msg.toLowerCase()) } window.myMoudle2 = {doSomething,doOtherthing} })(window)
-
闭包的缺点
- 函数执行后,函数内部的局部变量没有被释放,占用内存时间边长
- 容易造成内存泄露
- 解决:能不用闭包就不用闭包,及时释放
function fn1(){ var arr = new Array[100000] function fn2(){ console.log(arr.length) } return fn2 } var f= fn1() f() f=null //解决 ,回收闭包
内存溢出
当程序运行所需要的内存超过了剩余的内存会抛出内存溢出的错误
内存泄露
占用的内存没有及时释放
内存泄露积累多了就容易导致内存溢出
常见的内存泄露* 意外的全局变量 * 没有及时清理的循环定时器setInterval或回调函数 * 闭包
面试题
var name = 'The Window'
var object = {
name:'My Object',
getNameFun:function(){
return function(){
return this.name
}
}
}
alert(object.getNameFun()()) //The Window
var name = 'The Window'
var object = {
name:'My Object',
getNameFun:function(){
var that = this
return function(){
return that.name
}
}
}
alert(object.getNameFun()()) //My Object
function fun(n,o){
console.log(o)
return {
fun:function(m){
return fun(m,n)
}
}
}
var a = fun(0);a.fun(1);a.fun(3);
var b = fun(0).fun(1).fun(2).fun(3);
var c = fun(0).fun(1);c.fun(2);c.fun(3)
关于this对象
在闭包中使用this对象可能会导致一些问题
我们知道,this对象是在运行时基于函数的执行环境绑定的:
- 在全局函数中this=window,
- 而函数被作为某个对象的方法调用时,this等于那个对象。
不过匿名函数的执行环境具有全局性,因此this对象通常指向window。
但由于闭包的方式不同,这一点可能不会那么明显
var name = 'window'
var object = {
name:'object',
getNameFunc:function(){
return function(){
return this.name
}
}
}
alert(object.getNameFunc()())//window
为什么返回的匿名函数执行时没有取得其包含作用域(或外部作用域)的this对象呢?
- 每个函数在被调用时,其活动对象会自动取得两个特殊变量:this和arguments。
- 内部函数在搜索这两个变量时,只会搜索到其活动对象位置,因此永远不可能直接访问到外部函数中的这两个变量。
- 不过把外部作用域中的this对象保存在一个能够访问到的变量里,就可以让闭包访问该对象了。
var name = 'window'
var object = {
name:'object',
getNameFunc:function(){
var that = this
return function(){
return that.name
}
}
}
alert(object.getNameFunc()())//object
函数返回后,that依然引用者object,
所以调用object.getNameFunc()()返回"object"
this和arguments都存在者同样的问题。
如果访问作用域中的arguments对象,必须将该对象的引用保存到另一个闭包能够访问的变量中
在几种情况下,this的值可能意外地改变
var name = 'window'
var object = {
name:'object',
getName:function(){
return this.name
}
}
object.getName() //object
(object.getName)() //object
(object.getName = object.getName)() //window
函数式编程
js编程特点:
函数式编程和面向对象编程的混编语言
弱类型
编程灵活易学不可控
面向对象与函数式编程的关系
纯函数和函数缓存池
slice 纯函数
splice 非纯函数
function cacheFn(fn){
var cache = {};
return function(){
var args = JSON.stringify(arguments);
cache[args] = cache[args] ? cache[args]+'(来自缓存池)'
: fn.apply(fn,arguments)
return cache[args];
}
}
var sum = function(){
var res = 0
for(var i = 0; i<arguments.length; i++){
res +=arguments[i];
}
return res;
}
var test = cacheFn(sum)
console.log(test(1,2))
console.log(test(1,2))
函数组合,高阶函数,偏函数(左倾)
->饲养函数->compose
若干个纯函数,偏函数,柯理化函数组合成一个新的函数,形成数据传递,并实现一种有序执行的效果
function toUpperCase(str){
return str.toUpperCase()
}
function exclaim(str){
return str + '!';
}
// 高阶函数是一个接收函数作为参数或将函数作为输出返回的函数。
function compose(f,g){
return function(x){
return f(g(x)) //左倾函数
// 在 js 函数中,有一种函数叫偏函数( 左倾 ),
// 其原理是将一些函数组合封装到一个函数中,调用时可以按从右到左的顺序实现全部功能。
}
}
var f = compose(exclaim,toUpperCase)//右到左的顺序 左倾
console.log(f('hello'))
function testArgToArr(){
console.log(Array.prototype.slice.call(arguments))
// splice有副作用,影响原数组
console.log(Array.prototype.splice.call(arguments,0,arguments.length))
console.log(arguments)//length:0
}
testArgToArr(1,2,3)
- compose 参数比较多的情况
function toUpperCase(str){
return str.toUpperCase()
}
function split(str){
return str.split("")
}
function compose2(){
var args = Array.prototype.slice.call(arguments);
var len = args.length-1;
console.log(args[len])
return function(x){
var res =args[len](x);
console.log(res)
while(len--){
res = args[len](res)
}
return res;
}
}
var f = compose2(split,toUpperCase)
console.log(f('hello'))
- reduceRight
reduceRight() 方法的功能和 reduce() 功能是一样的,不同的是 reduceRight() 从数组的末尾向前将数组中的数组项做累加。
注意: reduce() 对于空数组是不会执行回调函数的。
function toUpperCase(str){
return str.toUpperCase()
}
function split(str){
return str.split("")
}
function compose3(){
var args = Array.prototype.slice.call(arguments);
return function(x){
return args.reduceRight((prev,cur)=>{
return cur(prev)
},x)
}
}
var f = compose3(split,toUpperCase)
console.log(f('hello'))
函数组合结合率associativity
函数组合内部参数无论如何再组合结果不会受到影响
var f = compose3(split,toUpperCase)
console.log(f('hello'))
var f = compose3(split,compose3(toUpperCase))
console.log(f('hello'))//(5) ['H', 'E', 'L', 'L', 'O']
// 结果不变
pointfree 编程风格指南
以上compose就叫做 Pointfree:不使用所要处理的值,只合成运算过程。中文可以译作"无值"风格。
Pointfree 的本质就是使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理
高阶函数
一个函数接收另一个函数作为参数变量的这个函数就是高阶函数
- map->数据处理函数
- reduce->归纳函数
function setDate(initVal,elem){
if(elem>3){
initVal.push(elem)
}
return initVal;
}
var newArr = [1,2,3,4,5,].reduce(setDate,[])
console.log(newArr) //[4, 5]
- 数组的扩展方法,计时器,sort,replace都是高阶函数
var test = function(a,b,fn){
return fn(a,b)
}
var res = test(1,2,(a,b)=>a+b);
console.log(res)
- 高阶函数如果只是为了执行另一个函数,这样是没意义的
- 函数返回的简化
var test = function(fn){
return doSth(function(data){
return fn(data)
})
}
function doSth(fn){
fn()
}
// 上面函数嵌套函数返回时没有意义的,直接看最后的返回
var test = fn(data)
函数柯里化
// 普通的add函数
function add(x, y) {
return x + y
}
// Currying后
function curryingAdd(x) {
return function (y) {
return x + y
}
}
add(1, 2) // 3
curryingAdd(1)(2) // 3
Currying是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,
并且返回接受余下的参数而且返回结果的新函数的技术。
- 正宗的currying以下情况都应该满足
function add(a,b,c){
return a+b+c
}
add(1,2,3)
add(1)(2)(3)
add(1,2)(3)
add(1)(2,3)
-
作用
- 参数复用
- 简化代码
- 提高维护性
- 功能单一化
- 函数延迟执行
- 功能内聚
- 降低耦合
- 降低代码重复
- 提高代码适用性
-
第一版
function add(a,b,c){
return a+b+c
}
function curry(fn){
var _arg = [].slice.call(arguments,1)
return function(){
var newArgs = _arg.concat([].slice.call(arguments))
console.log('newargs',newArgs)
return fn.apply(this,newArgs) //this指向没意义
}
}
var add2=curry(add,1,2)
add2(3)
curry(add,1)(2,3) //只能两个括号 因为curry只return了一次
- 第二版 递归
function add(a,b,c){
return a+b+c
}
function curry(fn,len){
var len = len || fn.length;
var func = function(fn){
var _arg = [].slice.call(arguments,1);
return function(){
var newArgs = _arg.concat([].slice.call(arguments))
return fn.apply(this,newArgs)
}
}
return function(){
var argLen = arguments.length;
if(argLen < len){
var formatedArr = [fn].concat([].slice.call(arguments));
return curry(func.apply(this,formatedArr),len-argLen)
}else{
return fn.apply(this,arguments)
}
}
}
var add2= curry(add)
add2(1)(2)(3)
add2(1,2)(3)
add2(1)(2,3)
偏函数
什么是函数的元->参数个数
有两个参数的函数->二元函数
-
什么是偏函数
使原函数变为元更少的函数 -
柯里化与偏函数的区别
-
柯里化:将一个多参数的函数转换成单个参数的函数,将n元函数转换成n个一元函数
-
偏函数:是固定一个函数的一个或多个参数,将n元函数转换成n-x元函数
- 典型偏函数
function add(a,b,c){
return a+b+c
}
var newadd = add.bind(null,1,2)
newadd(3)
- 自己封装
function add(a,b,c){
return a+b+c
}
Function.prototype.partial = function(){
var _self = this;
var _args = [].slice.call(arguments);
return function(){
var newArgs = _args.concat([].slice.call(arguments));
return _self.apply(null,newArgs)
}
}
var newadd = add.partial(1,2)
newadd(3)
惰性函数
- 单例模式->懒加载
var timeStamp = null
function getTimeStamp(){
if(timeStamp){ //每次都要进行if判断, 全局变量污染
return timeStamp
}else{
timeStamp = new Date().getTime();
return timeStamp
}
}
console.log(getTimeStamp())
- 使用匿名函数
var getTimeStamp = function(){
var timeStamp = new Date().getTime()
return function(){
return timeStamp
}
})()
console.log(getTimeStamp())
- 函数内部改变自身->惰性函数的机制
var getTimeStamp =(function(){
var timeStamp = new Date().getTime()
getTimeStamp = function(){
return timeStamp
}
// return timeStamp;
return getTimeStamp()
})
console.log(getTimeStamp())
惰性加载表示函数执行的分支只会在函数第一次调用的时候执行,
在第一次调用的过程中:
该函数被覆盖为另一个按照合适的方式执行的函数,
这样任何对原函数的调用就不用再经过执行的分支了
- 多功能函数滞后确定
function test(num){
switch(num){
case 1:
test = function(){
console.log(1)
}
break;
case 2:
test = function(){
console.log(2)
}
break;
case 3:
test = function(){
console.log(3)
}
break;
}
return test();
}
console.log(test(1))
console.log(test())
函数记忆,函数缓存
缓存函数
阶乘 n!=n*(n-1)
0!=1
function factorial(n){
if(n==0 || n==1){
return 1
}
return n * (factorial(n-1))
}
factorial(3)
var times = 0;
var cache = [];
function factorial(n){
times++;
if(cache[n]){
return cache[n]
}
if(n==0 || n==1){
cache[0] = 1
cache[1] = 1
}
return cache[n] = n * (factorial(n-1))
}
- 函数记忆
function factorial(n){
if(n==0 || n==1){
return 1
}
return cache[n] = n * (factorial(n-1))
}
function memorize(fn){
var cache = {};
return function(){
var k = [].join.call(arguments,',') //函数的参数为key
return cache[k] = cache[k] || fn.apply(this,arguments)
}
}
var f = memorize(factorial)
console.log(f(100))
console.log(f(100))
- 兔子数列 斐波那契数列 F(0)=1,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)
随着数列项数的增加,前一项与后一项之比越来越逼近黄金分割的数值 0.6180339887……
function fab(n){
return n<=2 ? 1 : fab(n-1)+fab(n-2)
}
function memorize(fn){
var cache = {};
return function(){
var k = [].join.call(arguments,',') //函数的参数为key
return cache[k] = cache[k] || fn.apply(this,arguments)
}
}
var f = memorize(fab)
console.log(f(8))
函数防抖,函数节流
- 防抖
- 在事件触发n秒后执行回调,延迟执行,如果n秒内多次触发,重新计时
- 应用案例:input输入时验证,表单提交,鼠标事件
- 节流
- n秒内事件只被触发一次
- 区别
防抖如果一直在n秒内频繁触发就永远不会执行,而节流n秒内总会执行一次
归类函数
- 单一归类
const user = [
{"id":"0","name":"张三","sex":0},
{"id":"1","name":"李四","sex":1},
{"id":"2","name":"王五","sex":0},
{"id":"3","name":"麻子","sex":1}
];
const sex = [
{"id":"0","sex":'男'},
{"id":"1","sex":'女'}
]
let cache = {};
sex.forEach((item)=>{
let id = item.id;
cache[id] = [];
user.forEach((u)=>{
let _sex = u.sex;
if(id == _sex){
cache[id].push(u)
}
})
})
console.log(cache)
- 复合归类
const hobby = [
{"id":1,"name":"足球"},
{"id":2,"name":"游泳"},
];
const person =[
{"name":"jack","hobby":"1,2"}
]
- 归类函数封装
function sortDatas(sort,data){
var cache={};
return function(foreign_key,sortType){
sort.forEach(s => {
var _id = sort.id;
cache[_id] = [];
data.forEach((d)=>{
var foreign_val = d[foreign_key];
switch(sortType){
case 'single':
if(foreign_val==_id){
cache[_id].push(d)
}
break;
case 'multi':
if(foreign_val.indexOf(_id)!=-1){
cache[_id].push(d)
}
break;
default:
break;
}
})
});
}
}