重要概念
-
定义
- 匿名函数
- 具名函数
- 箭头函数
-
词法作用域(也叫静态作用域)
var global1 = 1 function fn1(param1){ var local1 = 'local1' var local2 = 'local2') function fn2(param2){ var local2 = 'inner local2' console.log(local1) console.log(local2) } function fn3(){ var local2 = 'fn3 local2' fn2(local2) } }
- 深入阅读:
- javascript的词法作用域
- 静态作用域与动态作用域
- Abstract Syntax Tree 抽象语法树简介
- 深入阅读:
-
call back
- 普通调用 1+1+1
- 嵌套调用 1>2>3
- 递归
-
this & arguments
- 重要:this 就是 call 的第一个参数!call 的其他参数统称为 arguments。如果你记住了这个规则,那么网上其他关于 this 的解释在此时都变得很啰嗦了。
- 如果传进去的不是对象,而是一个数字1,那么会被变成对象,如new Number(1)
- this 是隐藏的第一个参数,且一般是对象(如果不是对象,就显得很没有意义了
function f(){ console.log(this) console.log(arguments) } f.call() // window f.call({name:'frank'}) // {name: 'frank'}, [] f.call({name:'frank'},1) // {name: 'frank'}, [1] f.call({name:'frank'},1,2) // {name: 'frank'}, [1,2]
- this 为什么必须是对象
因为 this 就是函数与对象之间的羁绊var person = { name: 'frank', sayHi: function(person){ console.log('Hi, I am' + person.name) }, sayBye: function(person){ console.log('Bye, I am' + person.name) }, say: function(person, word){ console.log(word + ', I am' + person.name) } } person.sayHi(person) person.sayBye(person) person.say(person, 'How are you') // 能不能变成 person.sayHi() person.sayBye() person.say('How are you') // 那么源代码就要改了 var person = { name: 'frank', sayHi: function(){ console.log('Hi, I am' + this.name) }, sayBye: function(){ console.log('Bye, I am' + this.name) }, say: function(word){ console.log(word + ', I am' + this.name) } } // 如果你不想吃语法糖 person.sayHi.call(person) person.sayBye.call(person) person.say.call(person, 'How are you') // 还是回到那句话:this 是 call 的第一个参数 // this 是参数,所以,只有在调用的时候才能确定 person.sayHi.call({name:'haha'}) // 这时 sayHi 里面的 this 就不是 person 了 // this 真的很不靠谱 // 新手疑惑的两种写法 var fn = person.sayHi person.sayHi() // this === person fn() // this === window
-
call/apply
- fn.call(asThis, p1,p2) 是函数的正常调用方式
- 当你不确定参数的个数时,就使用 apply
- fn.apply(asThis, params)
bind
call 和 apply 是直接调用函数,而 bind 则是返回一个新函数(并没有调用原来的函数),这个新函数会 call 原来的函数,call 的参数由你指定return
每个函数都有 return
如果你不写 return,就相当于写了 return undefined-
柯里化 / 高阶函数
返回函数的函数- 柯里化:将 f(x,y) 变成 f(x=1)(y) 或 f(y=1)
//柯里化之前 function sum(x,y){ return x+y } //柯里化之后 function addOne(y){ return sum(1, y) } //柯里化之前 function Handlebar(template, data){ return template.replace('{{name}}', data.name) } //柯里化之后 function Handlebar(template){ return function(data){ return template.replace('{{name}}', data.name) } }
- 柯里化可以将真实计算拖延到最后再做
- 关于柯里化的高级文章:
- http://www.yinwang.org/blog-cn/2013/04/02/currying
- https://zhuanlan.zhihu.com/p/31271179
- 高阶函数:
- 在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入:forEach sort map filter reduce
- 输出一个函数:lodash.curry
- 不过它也可以同时满足两个条件:Function.prototype.bind
- 在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
- 柯里化:将 f(x,y) 变成 f(x=1)(y) 或 f(y=1)
回调
名词形式:被当做参数的函数就是回调
动词形式:调用这个回调
注意回调跟异步没有任何关系构造函数
返回对象的函数就是构造函数
一般首字母大写
课后题答案:
function curry(func , fixedParams){
if ( !Array.isArray(fixedParams) ) { fixedParams = [ ] }
return function(){
let newParams = Array.prototype.slice.call(arguments); // 新传的所有参数
if ( (fixedParams.length+newParams.length) < func.length ) {
return curry(func , fixedParams.concat(newParams));
}else{
return func.apply(undefined, fixedParams.concat(newParams));
}
};
函数声明
- 匿名函数
- 一般直接声明匿名函数会报错的,我们将匿名函数赋给一个变量,这个变量记录的是堆内存中的地址,函数体是记录在堆内存中
- 匿名函数的名称跟赋给的变量名称相同,即便是匿名函数,但是有name属性
-
- 具名函数
- 具名函数是具有名字的函数,这个名字其实是一个变量,这个变量具有作用域
- 当我们直接写一个具名函数fn3,那么在整个外围的作用域里面都能访问fn3这个变量
- 当我们将一个具名函数在fn4赋值给一个变量fn5,那么fn4其实只能在函数作用域中访问(即高亮部分),函数作用域之外就无法访问,但是这个变量fn5就可以在外层访问
-
- 箭头函数
- 箭头函数一个参数不用使用括号包起来,多个参数需要使用括号包起来
- 箭头函数语法块只有一句好不需要用大括号包起来,不需要写return,build自动将语句执行的结构返回;但是多个语句需要用大括号包起来,同时将return的值表明。
词法作用域
- 当浏览器看到下面这个语法的时候,不会马上执行它,而是会去做一个叫抽象词法树的东西。将下面的代码变成一棵树状的结构,以便于后续的执行
var global1 = 1 function fn1(param1){ var local1 = 'local1' var local2 = 'local2' function fn2(params){ var local2 = 'inner local2' console.log(local1) console.log(local2) } function fn3(){ var local2 = 'fn3 local2' fn2(local2) } }
-
函数中传入的形参也是一种变量声明,找寻变量是根据词法树来确定的,本级找不到的话,就往上一级去找。一个函数里面能访问哪些变量,在做词法分析的时候就已经确定了。此法分析只分析语义,即到底能访问的是哪个变量等,不分析具体的值
Call Stack
- Stack就是栈,即先进后出。跟队列正好相反。
- 浏览器拿到代码的第一步是做词法解析,会将所有声明操作进行前置。
- 推荐一个网站,可以查看Stack的运行方式:http://latentflip.com/loupe/?code=JC5vbignYnV0dG9uJywgJ2NsaWNrJywgZnVuY3Rpb24gb25DbGljaygpIHsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZXIoKSB7CiAgICAgICAgY29uc29sZS5sb2coJ1lvdSBjbGlja2VkIHRoZSBidXR0b24hJyk7ICAgIAogICAgfSwgMjAwMCk7Cn0pOwoKY29uc29sZS5sb2coIkhpISIpOwoKc2V0VGltZW91dChmdW5jdGlvbiB0aW1lb3V0KCkgewogICAgY29uc29sZS5sb2coIkNsaWNrIHRoZSBidXR0b24hIik7Cn0sIDUwMDApOwoKY29uc29sZS5sb2coIldlbGNvbWUgdG8gbG91cGUuIik7!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4=
- 每一次都会记录进入的位置,一个位置里面可以有其他位置进入。从一个位置进入,就还从这个位置出来,这就形成了先进后出。
-
-
递归就是这种无限进入下一个位置,我们举个例子,菲薄拉起数列,每一项是前面两项的和。
- 总结call Stack:
- JS是单线程,在执行一长串代码的时候,会将当前环境都记住,比如能访问哪些变量等。
- 突然看到一个函数,这个时候就切换环境,因为它要进入这个函数,因为函数的代码并不在原先的环境中,
- 为了记住要回到原先的环境中,就做一个记录,可能会有很多层的记号,将这些记号放入栈里面,叫做调用栈,只要多一层调用栈,就出现一个记录,进入新的函数进行执行
- 如果函数里面还有函数,就继续进入。每次回来,就先回最后进入的那个标记
- call Stack是一个理论上的知识,可以帮助我们理解函数的调用。
this & arguments
- 这个跟调用栈具有很大的关系,在进入一个函数的时候,除了需要记录下进入的地址,还得记录下传给函数的参数有哪些。
- call的第一个参数就是this,剩下的参数就是arguments,不传就是空数组
-
为什么要使用call,而不是f(),因为f()是阉割版的call。当使用f(1)的时候,1不被当做this,这是因为浏览器会猜测this是window,将1作为参数。而使用call则是将函数的this指明了,所以不要使用f()
-
传进去一个10,就会将10转成number对象。this是函数与对象之间的羁绊。
- person.sayHi()与person.sayHi.call(person)是等价的,前者会通过词法解析变成后面这样的。两者的意思都是以person为this调用sayHi。
-
JS之父为了让我们person.sayHi()能被解析,就发明了this。写了点就将点前面的当做this,没有写点,就将windows作为this
-
this就是让函数有一个可依托的对象,使用call一定要将第一个参数传进去,如果不依赖任何this,就可以传入undefined/null,否则可能会报错
-
有种情况没法使用call来写,比如求和所有参数。无法确定参数的个数。可以使用循环遍历。但是使用apply更加方便,apply的第二个参数就是arguments,是一个数组,数组中有多少项都是可以的
bind
- call和apply都是直接调用函数,而bind则是没有调用函数,而是返回一个新的函数
-
onclick后面会绑定一个函数,这个函数的this会在浏览器的源代码中写好。我们一眼看是看不出来this是什么的,因为this等价于call的第一个参数
- 我们有一个对象叫view,对象中有一个元素是element,有一个bindEvents的方法,这个方法所在的大环境值view,所以this就是view;但是里面onclick事件的this其实是被点击的元素,即触发事件的元素我们绑定事件的时候,需要将this指定为view.element,为了明确指定,我们使用_this拿到外层的this。使用onclick函数不能直接用,因为我们需要给其创造一个this。我们不能直接写this.element.onclick = this.onClick,因为后面的this是onclick函数指定的点击对象,可能是div等,但他们不具有onClick函数。我们需要使用一个函数包起来,去更改一下this,找到对应的onClick函数
var view = { element: $('#div1'), bindEvents: function(){ var _this = this this.element.onclick = function(){ _this.onClick.call(_this) } }, onClick: function(){ alert('你点击了我!') } }
- 既然是指定的,为何不使用view代替this呢,这样不是更加明确吗,省得this不明确带来的各种麻烦
var view = { element: $('#div1'), bindEvents: function(){ view.onClick = function(){ view.onclick } }, onClick: function(){ alert('你点击了我!') } }
- 我们可以使用bind做同样的事情,即更改onclick函数指定的this到想要的地方去。这样写法的意思就是.bind会返回一个新的函数,这个函数会将bind前面的函数(这边就是this.onClick)包起来,bind的作用就是往onClick后面加一个call
var view = { element: $('#div1'), bindEvents: function(){ this.element.onclick = this.onClick.bind(this) }, onClick: function(){ alert('你点击了我!') } }
- 上面this.onClick.bind(this)其实就是实现了
function(){
this.onClick.call(this)
}
而且this.onClick.bind(this)的this没有进入一个新的函数,所以我们不需要声明一下下划线_this,bind里面是什么,就相当于函数里面的call里面是什么,而现在的使用bind(this)跟外面的this是一样的,所以不需要使用下划线this - 这样,我们就达到这个目的了,当我们调用onclick函数的时候,会去调用onClick函数,调用的方式就是在onClick后面加一个call,bind的作用就是往onclick后面加一个call,但是不是现在加,而是在调用bind后的新函数的时候加
- bind返回的是一个函数,这个函数的作用就是调用之前的函数,之前的函数后面会接一个apply或者call,里面的参数就是你写给bind的参数
var view = { element: $('#div1'), bindEvents: function(){ this.onClick.bind = function(x,y,z){ var oldFn = this // 这个this就是外面的this.onClick return function(){ oldFn.call(x,y,z) } } this.element.onclick = this.onClick.bind(this) }, onClick: function(){ this.element.addClass('active') } }
柯里化
-
柯里化就是将多个参数中一部分固定下来,变成一种偏函数,得到的一种新函数
- 意义:柯里化在模板引擎中的使用是可以等到数据全部齐全之后再进行渲染
- 可以用来做惰性求值:我们在调第一个函数的时候其实啥也没做,我们在真正使用的时候再去生效
- 用的不是很多的
高阶函数
- 接收函数作为参数或者返回的是函数,那么就是高阶函数
- 接收函数
- array.sort(function(a, b){a-b})
- array.forEach(function(a){})
- array.map(function()}{})
- array.filter(function(){})
- array.reduce(function(){})
- 返回函数
- fn.bind.call(fn, {}, 1,2,3):bind是接收一个函数fn的,这个fn会被bind包装成另一个新的函数,这个新的函数会接受一些参数,bind的call的第二个参数,会作为bind的call的第一个参数fn的call的第一个参数,即其this,后面的1,2,3就是fn剩余的参数。
- 意义:将函数进行任意组合,这个在react组件里面用的很多
-
例子:求和所有偶数的和,几种写法不一样,使用高阶函数也是很直观的,还简洁
回调和构造函数
- callback,函数作为参数传入函数中,并调用了
- 回调跟异步没有任何关系
-
返回对象的函数就是构造函数,构造函数中可以不return,因为会默认返回对象的,因为我们会使用new来调用构造函数
- 函数很少返回对象
箭头函数
-
箭头函数没有this,箭头函数里面的this就是外面的this
-
箭头函数是无法指定this的
牛刀小试
- 请写出一个柯里化其他函数的参数,这个函数能够将接受多个参数的函数变成多个接受一个参数的函数。
- 我们在写函数的时候需要弄清楚两个事情,一个是输入,一个是输出,弄清楚之后再写中间的逻辑
- 一种最为简单的实现方式
function sum(x, y){ return x + y } function curry(fn, p1){ return function(p2){ return fn.call(undefined, p1, p2) } } var addOne = curry(sum, 1) addOne(2) // 3 addOne(3) // 4
- 上面的写法不满足fn(1)(2)这样子,怎么改?
function sum(x, y){ return x + y } function curry(fn){ return function(p1){ return function(p2){ return fn.call(undefined, p1, p2) } } } curry(fn)(1)(2)
- 只有将买所有的值都传到位了,才会最终返回一个值
- 如果我们不知道接受的参数个数呢?
- 函数的length属性就反映着传入函数的参数个数
-
function curry(func, fixedParams){ if(!Array.isArray(fixedParams)){fixedParams = []} return function(){ let newParams = Array.prototype.slice.call(arguments); if((fixedParams.length + newParams.length) < func.length){ return curry(func, fixedParams.concat(newParams)); }else{ return func.apply(undefined, fixedParams.concat(newParams)); } }; }