定义
JS中函数的声明方式有以下三种
匿名函数
匿名函数就是声明的时候不要给名字
function(){
return 1
}
直接运行会报错,因为声明了一个函数但是又不能引用它,相当于一个废话,只能给它一个引用
var fn = function(){
return 1
}
JS中函数是对象,对象是存在堆内存中,一般存就只存函数的地址,所以变量fn就存了函数的地址而不是函数本身
如果是
var fn2 = fn
那么fn2和fn存的都是这个函数的内存地址
fn.name // fn
fn2.name // fn
匿名函数但是它有name,name就是存函数地址的变量名
具名函数
顾名思义,具名函数就是具有名字的函数
// fn3是变量
function fn3(){
return 3
}
var fn5 = function fn4(){
}
console.log(fn4) // undefined
fn3() // 3
我们知道fn3的作用域是全局,但是如果我们把一个具名函数赋值给了一个变量,那么它的作用域就仅仅是这个具名函数内部
那么fn5.name是多少?
=> 是fn4
箭头函数
只有一个参数
var fn6 = i => i+1
多个参数
// 不能这样写
var fn6 = i,j => i+j
// 要把参数括起来
var fn6 = (i,j) => i+j
函数体是多个语句
// 不能这样
var fn6 = (i,j) => i+j;console.log(1)
// 加括号括起来,同时显示的指定return的值,一个语句return可以省略
var fn6 = (i,j) => {console.log(1);return i+j}
箭头函数同普通函数的区别在于: this
词法作用域
也叫静态作用域
例子:
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)
}
}
当浏览器拿到这段代码不会马上去执行,它会先做一个『抽象词法树』(先分析词法是否正确 )
如果要在函数fn2中找变量local1,从右边的词法树下发现fn2下面只有local2找不到local1,于是就去它的『父作用域』去找发现fn1下面有个local1,于是就找到了
对应在代码中的作用域
比如,在fn2中找变量locla2,发现有local2,就不继续往它的上一层找。如果找不到就往的它上一层(fn1)找,如果还是找不到就再往它的上一层找(widow)
思考:调用fn3的时候,发现里面调用了f2,同时把f3里面的local2传给了fn2调用,那么会不会影响判断fn2里面的local2呢?
=> 是不会的!一个函数里面能够访问哪些变量,在做词法分析的时候已经确定了,与函数调用不调用没有任何关系!
注意:词法分析仅仅只适用于分析作用域中的变量是不是这个变量,而不是分析和这个变量的值是不是这个变量的值
例子:
var a = 1
function b(){
console.log(a) // 这里的a就是外面的a,因为它自己作用域内没有a
}
但是函数里面a的值是不是就一定也是外面a的值1呢?
=> 不是
var a = 1
function b(){
console.log(a) // a=2
}
a = 2
b()
因此,词法作用域只能确定这个a是不是那个a,但是不能确定这个a的值是不是那个a的值,这就是词法作用域,确定的是变量之间的关系
call stack (调用栈)
stack:栈(先进后出),理解这部分有助于理解函数的调用过程。
function a(){
console.log('a')
return 'a'
}
function b(){
console.log('b')
return 'b'
}
function c(){
console.log('c')
return 'c'
}
a()
b()
c()
通过上面的例子我们可以清晰的看出代码执行中栈内存发生的过程
浏览器拿到这段代码,首先不是运行代码,而是发现有哪些变量声明,然后把他们提到前面去,即使把函数的声明放到最后,浏览器还是会把他们提到最前面,这一步做的是词法解析,接着才去执行
看下下过程:
执行a(),记住a()的位置(在19行离开),进入a里面,记住console.log(a )(它也是一个函数)的位置(第2行),进入函数console.log(a )里面执行打印a,然后再回到之前标记的离开console.log(a )的位置,然后return a(从函数里离开),回到之前标记的a()的离开的位置(在19行离开),接着按照同样的思路执行函数b、c
因此call stack,记的是从哪个位置离开的(进入函数内部之前的位置),当进入新的函数把内容执行完了后,return的时候就是return到call stack 最上面记录的位置,也就是执行函数调用时的位置,return后面的值就是回到标记位置时带回来的值,也是执行函数调用后的结果。
2.函数的嵌套调用
function a(){
console.log('a1')
b()
console.log('a2')
return 'a'
}
function b(){
console.log('b1')
c()
console.log('b2')
return 'b'
}
function c(){
console.log('c')
return 'c'
}
a()
console.log('end')
3.函数的递归调用
function fab(n){
console.log('start calc fab '+ n)
if(n>=3){
return fab(n-1) + fab(n-2)
}else{
return 1
}
}
fab(5)
求feb5就要求feb4,求feb4就要求feb3,求feb3就要求feb2和feb1,这样就知道了feb3
再求feb(2),就知道了feb4
feb5等于feb4+ feb3,还得求feb3,求feb3就要求feb2和feb1,这样就知道feb3
feb5也就知道了
start calc fab 5
start calc fab 4
start calc fab 3
start calc fab 2
start calc fab 1
start calc fab 2
start calc fab 3
start calc fab 2
start calc fab 1
总结:
call stack 到底在做什么?由于JS是单线程的,因此它在执行一长串代码的时候就要记住当前的环境(比如在作用域内能访问哪些变量)。当看到函数,就要切换环境(因为它要进入函数,函数的代码并不在这里,存在另外一个内存中),在进入这个函数之前它可能会忘记怎么回来,这时候它就在这个地方做了个记号,但是它要做很多记号,因为函数里面还有函数调用,因此它要把每一层的记号放到一个栈的最上面,这个栈叫调用栈(call stack),只要它进入一层调用栈,那么栈里面就会多一个它进入时的记录,接着就进入新的函数去执行函数体的代码,新的函数里如果还有函数调用,它就把进入第二个函数之前的位置放到栈的最上面,每次回来的时候就回到最近一次离开时的位置,就像是『盗梦空间』一样,如果你现在在第三层梦,要想从梦里走出来, 就要先退出第三层,然后是退出第二层梦,再退出第一层梦,先进后出,这个和队列正好相反,先进的第一层梦,但是最后才从第一层梦退出来
this 和 arguments
在进入函数的时候除了要记录进入时的位置(地址),还要记住传给函数的参数有哪些
this 就是 call 的第一个参数!call 的其他参数统称为arguments(伪数组)
this 是call()隐藏的第一个参数,且一般是对象(如果不是对象,也会帮你转成对象;如果不是对象,就显得很没有意义了)
例子:
function f(){
console.log(this)
console.log(arguments)
}
f.call() // window []
f.call({name:'lee'}) // {name: 'lee'}, []
f.call({name:'lee'},1) // {name: 'lee'}, [1]
f.call({name:'lee'},1,2) // {name: 'lee'}, [1,2]
f.call() === f()
当执行f.call()的时候第一件事把调用时候的位置记录到call stack (调用栈)最上面,接着就可以直接进入函数吗?不可以,好比进入别人门之前还要准备礼物(2个东西),第一个东西就是this,如果 f.call()没有写this就是undefined,浏览器会把它改成window,第二个要准备的东西就是arguments的伪数组,如果没有准备,argumnets就是'[]'空数组
注意: 控制台中浏览器运行的代码的结果中的Window实际上是window;
总结:函数在被调用的一瞬间发生的事
- 记录下函数被调用的位置,放到调用栈的最上面
- 传this,可传可不传,不传就会默认的变成window(在浏览器中)
- 传arguments,不传就是空数组,传什么就把什么放到这个数组里
为什么要用f.call()的形式调用函数而不是f()
=> 因为f() 是阉割版的f.call()
如果用f.call() ,就会知道this是什么,如果用f()这种形式调用函数,想知道this是什么就会变得模糊
例如:
function f(){
console.log(this)
console.log(arguments)
}
f() // window []
f(1) // window [1]
f():如果是这样调用函数,就会很疑惑this为什么是window
f(1): :如果是这样调用函数为什么arguments变成了[1]
为什么this必须是对象?
function f(){
console.log(this)
console.log(arguments)
}
f.call(10,1) // Number{10}
我们发现即使是指定this为数字10,也会被new Number()了一下,变成了Number对象而不是数字10,为什么会变成这样?
因为this就是函数与对象之间的羁绊
假如这个世界上没有this会怎么样?会不会变得很奇怪?什么情况下才会需要this?
例子:
var person = {
name: 'lee',
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)
}
}
世界上没有this的代码的写法:这样写就很烦,能不能直接访问"点"前面的东西,这样就不用传person参数,点前面是什么,就把它作为参数传进去?
person.sayHi(person)
person.sayBye(person)
person.say(person, 'How are you')
JS为了满足部分人想偷懒的想法,允许了下面这种写法
person.sayHi()
person.sayBye()
person.say('How are you')
"我没传参数给你,但是定义函数的时候却收到了参数",这就很矛盾了
var person = {
name: 'lee',
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)
}
}
于是就有了下面的解决办法:
单独的给第一个参数一个关键字this,既然不传person,那函数里面也不要接受person了
这时候函数里面也不能用person.name了,得用一个关键字代替你传进来的参数person,于是就有了this,this相当于一个占位符,我不知道你传了什么参数进来,我只根据点前面的对象来判断this,点前面是person,那么this就是person
var person = {
name: 'lee',
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)
}
}
至此,JS就做到了this可以访问点前面的东西,这就是一个语法糖
person.sayHi() // 以person为this来调用sayHi
person.sayBye()
person.say('How are you')
上面这种写法的目的不就是为了指定this吗?为什么不像下面这样写,下面这种写法同上面这种写法是一样的
// 没有语法糖的写法
person.sayHi.call(person) // 以person为this来调用sayHi
person.sayBye.call(person)
person.say.call(person, 'How are you')
再来捋一下:
var person = {
name: 'lee',
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() // Hi,i am lee
// window.name='xxx'
var fn = person.sayHi
fn()
这里直接写fn()效果是不是同person.sayHi() 一样呢?
=> 不一样!这时JS分析不出来你想要使用哪个对象来调用sayHi()
因此执行fn()的结果就是Hi I am
注意下面的函数同person和sayHi都没有关系,它就只是一个函数,函数只有输入和输出,没有所属的对象的说法
function(){
console.log('Hi, I am' + this.name)
}
如果加一句window.name='xxx'
结果 Hi, I am xxx,也就是说fn()里的this就是window,打印出来的就是window.name
如果运行person.sayHi.call({name:'yyy'})
结果: Hi, I am yyy
如果运行person.sayHi.call({name:'zzz'})
结果: Hi, I am zzz
以上就说明
function(){
console.log('Hi, I am' + this.name)
}
函数是独立的存在,同sayHi和person是没有任何关系,它就是和call有关系,call传什么this进来,函数里面的this就是什么
this 是 call 的第一个参数,this 是参数,所以,只有在调用的时候才能确定
如果不用call的方式调用函数,而是用用阉割版的方式调用:person.sayHi()会不小心的把person当做this
因此只要用call的方式调用函数,就不会有那么多模糊不清的东西了
person.sayHi() 等价于 person.sayHi.call(person)
fn() 等价于 fn.call()
person.sayHi() 第一眼并不知道this是什么
person.sayHi.call(person) 第一眼知道this是person
fn() 第一眼并不知道this是什么
fn.call() 不传第一个参数this就是是undefined,浏览器环境下会变成window
this的意义就是让函数有一个可依托的对象
完整的代码:
var person = {
name: 'lee',
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)
}
}
// 世界上没有this的代码的写法
person.sayHi(person)
person.sayBye(person)
person.say(person, 'How are you')
// 想偷懒,能不能变成下面这样,能不能直接访问"点"前面的东西,这样就不用传person参数,点前面是什么,就把它作为参数传进去?
person.sayHi()
person.sayBye()
person.say('How are you')
// 那么源代码就要改了
var person = {
name: 'lee',
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
再来看一个例子:
function sum(x,y){
return x + y
}
sum.call(undefined,1,2) // 3
sum.call(1,4) // NaN
call的第一个参数永远是this,如果不想传this就给个undefined/null来占位
sum.call(1,4)这种写法就变成了this是1,4是x,y是undefined,因此结果就是NaN
apply
apply就是call的另外一个版本
function sum(){
var n = 0
for(var i=0;i<arguments.length;i++){
n+ = arguments[i]
}
return n
}
sum(1,2,3,4) // 10
但是有一种场景是没有办法用call来写,求所有参数的和(不知道有多少参数)
var a = [1,2,3,4,5,6,7,8] // 不知道有多少个
sum.call(undefined,a[0],a[1],a[2]...) // 这里我没法写怎么办
即使知道有多少个,也不能傻傻的写sum.call(undefined,a[0],a[1],a[2]...a[7])
这时bind就应运而生了,解决参数不固定或者参数太长
var a = [1,2,3,4,5,6,7,8]
sum.apply(undefined,a) // 36
因此不管a有多少项,都能求出和,bind的第二个参数就是数组
总结:
fn.call(asThis, p1,p2) 是函数的正常调用方式
当你不确定参数的个数时,就使用 apply
fn.apply(asThis, params)
bind
call 和 apply 是直接调用函数,而 bind 则是返回一个新函数(并没有调用原来的函数),这个新函数会 call 原来的函数,call 的参数由你指定。
var view = {
element: 'div',
bindEvents: function(){
// 1处的this
this.element.onclick = function(){ // 一般来说this是view,意外情况就是用call显示的指定
// 2 处的this
this.onClick() // 这里的this是包裹它的function被call的时候传的第一个参数
}//.call()
},
onClick: function(){
}
}
element被点击的时候理应被调用,谁调用?
=> 浏览器调用,浏览器调用这个函数的时候用的是call的形式
function(){
this.onClick() // div
}.call()
浏览器用call()的方式调用了这个函数,那浏览器给call传的第一个参数是什么?
我们查下文档:
target.onclick = functionRef;
functionRef是函数名或函数表达式.该函数接收一个MouseEvent对象作为其唯一参数。在函数中,this将是触发事件的元素。
所以,this为被点击的元素也就是那个div
那问题来了:2处的this是div就不对了,我们想要2处的this是1处的this(也就是view怎么办?)
于是就有人提出在1处的地方使用一个变量_this记住1处的this,再到2处使用这个变量问题不就解决了吗
var view = {
element: 'div',
bindEvents: function(){
var _this = this
// 1处的this
this.element.onclick = function(){
// 2 处的this
_this.onClick()
}
},
onClick: function(){
this.element.addClass('active')
}
}
既然都是用变量(this是参数),又何必用_this变量存另外一个变量,而且弄一个"假猴"来绕晕自己,为什么不明明白白的指定
var view = {
element: 'div',
bindEvents: function(){
// 1处的this
this.element.onclick = function(){
// 2 处的this
view.onClick() // 这样就看出来了
}
},
onClick: function(){
this.element.addClass('active')
}
}
当然使用_this来存this也是一种方法(this是无法确定的东西,不到万不得已不要使用this)
官方给了另外的解决办法
var view = {
element: 'div',
bindEvents: function(){
// 此处的2个this在同一作用域下,都是指view
this.element.onclick = this.onClick.bind(this)
},
onClick: function(){
this.element.addClass('active')
}
}
也就是说
this.element.onclick = this.onClick.bind(this)
等于
var _this = this
this.element.onclick = function(){
_this.onClick.call(_this) // 把_this当做自己的this
}
捋一下思路:
我们真正想要的是onClick,但是又不得不写一个新的函数,在新的函数里call onClick
function(){
_this.onClick.call(_this)
}
如果能这样写就好了:
this.element.onclick = this.onClick(this)
但是不能这样写,这样onClick被调用的时候的this就是那个div了
怎么办呢?就是用一个函数把另外我们想要调用的函数包起来,这样在调用的时候就可以指定它里面的this
var view = {
element: 'div',
bindEvents: function(){
var _this = this
this.element.onclick = function(){_this.onClick.call(_this) }
},
onClick: function(){
this.element.addClass('active')
}
}
看下bind做了什么
当我们写了下面的代码
this.element.onclick = this.onClick.bind(this) //A处
bind实际上返回了一个新的函数并调用了这个返回了的函数(浏览器调用 ),你在A处给bind传了什么参数,那么B处就是什么参数(A处的this就是_this也就是view)
return function(){
this.onClick.call(this) // B处
}
再看下bind的伪代码
var view = {
element: 'div',
bindEvents: function(){
this.onClick.bind = function(x,y,z){
var oldFn = this // 也就是外面的this.Click
return function(){ //浏览器会调用这个函数
oldFn.call(x,y,z)
}
}
this.element.onclick = this.onClick.bind(this)
},
onClick: function(){
this.element.addClass('active')
}
}
总结:
this.element.onclick = this.onClick.bind(this)
- this永远是call()的第一个参数
- bind 是返回一个新函数(并被浏览器自动的去调用),并没有直接调原来的函数onClick,这个新函数会 call 原来的函数onClick,call 的参数由你指定,传给bind的第一个参数会作为onClick.call()的第一个参数,bind的其他参数会作为onClick.call()的其他参数
科里化 & 高阶函数
返回函数的函数
柯里化
在数学中
z = f(x,y) = x + 2y // z是关于x和y的函数
g = f(x=1)(y) = x + 2y // g是关于y的函数,因为x已经被确定了
g和z的关系:g是z的偏函数,只是它的一部分
柯里化:把一个函数其中的一个参数固定下来得到一个新的函数
例子:
function sum(x,y){
return x+y
}
function addOne(y){
return sum(1,y) // 把sum的x固定为1
}
function addTwo(y){
return sum(2,y) // 把sum的x固定为1
}
addOne(4) // 5
addTwo(4) // 6
addOne 和 addtwo 做的事情就是柯里化
柯里化的意义是什么?
以早期的模板引擎为例(给一个模板字符串,再给一个data,就能返回一个html)
var Handerber = function(template,data){
return template.replace('{{name}}',data.name)
}
Handerber('<h1>Hi! I am {{name}}</h1>',{name:'lee'})
// 运行结果: <h1>Hi! I am lee</h1>
这样存在一个问题,如果有10000个不同的name就得重复写10000次模板字符串
Handerber('<h1>Hi! I am {{name}}</h1>',{name:'zhang'})
Handerber('<h1>Hi! I am {{name}}</h1>',{name:'wang'})
这个模板会被经常使用,是否可以做到复用这个模板?
var Handerber = function(template,data){
return template.replace('{{name}}',data.name)
}
var template = '<h1>Hi! I am {{name}}</h1>'
Handerber(template,{name:'zhang'})
// 运行结果: <h1>Hi! I am zhang</h1>
函数式编程中不会频繁的声明变量,而是声明一个科里化的函数,它需要被调用2次
第一次:只是返回一个函数
function Handerbar2(template){
return function(data){
return template.replace('{{name}}',data.name)
}
}
var t = Handerbar2('<h1>Hi! I am {{name}}</h1>')
t
t就是一个函数
ƒ (data){
return template.replace('{{name}}',data.name)
}
第二次: 模板才会和data结合在一起
function Handerbar2(template){
return function(data){
return template.replace('{{name}}',data.name)
}
}
var t = Handerbar2('<h1>Hi! I am {{name}}</h1>')
t({name:'wang'})
// '<h1>Hi! I am wang</h1>'
以上就是科里化在模板引擎中的使用
当然也可以用于惰性求值,第一次的var t = Handerbar2('<h1>Hi! I am {{name}}</h1>') 其实什么也没做
只有在第二次调用的时候才真正的生效t({name:'wang'})
一般用于对模板进行很重的操作,只有在需要的时候才第二次调用,这就叫惰性求值
科里化在惰性求值的时候还是有意义的
总结:
//柯里化之前
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)
}
}
高阶函数
在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
a. 接受一个或多个函数作为输入
b. 输出一个函数
c. 不过它常常同时满足两个条件
举例:接受一个或多个函数作为输入
array.sort(function(a,b){a-b}) array.sort.call(array,fn)
array.forEach(function(a){}) array.forEach.call(array,fn)
array.map(function(){}) array.map.call(array,fn)
array.filter(function(){}) array.filter.call(array,fn)
array.reduce(function(){}) array.reduce.call(array,fn)
备注:
fn为对数组进行处理的函数(对数组所有的操作都放在fn里)
举例:输出一个函数
fn.bind.call(fn,{},1,2,3)
备注:
bind接受一个函数fn作为输入,会返回一个新的函数,这个新的函数会调用fn(浏览器自动去调用),bind的第二个参数会作为fn.call()的第一个参数(也就是指定fn的this),参数1、2、3就是fn接收的其他参数
举例:既输入函数又输出函数
=> bind
高阶函数有什么用呢?
=> 它可以将函数任意的组合
例子:找出数组中所有的偶数并求和
// 循环的方式
array = [1,2,3,4,5,6,7,8]
var sum = 0
for(var i=0;i<array.length;i++){
if(array[i]%2===0){
sum += array[i]
}
}
// 面向对象的方式
array.filter(function(n){n%2===0}) // [2,4,6,8]
.reduce(function(pre,next){return pre+next},0) // 0作为callback中pre的初始值
// 函数式的方式
reduce(filter(function(n){n%2===0}),function(pre,next){return pre+next},0)
例子:找出数组中的所有单数并排序
array = [1,2,3,4,5,6,7,8]
sort(filter(array,function(n){n%2===1}),function(a,b){return a-b})
这些函数在underscore.js或者lodash里都能找到该怎么写
React中每个组件可以是一个纯函数,组件和组件可以相互转换和使用,有了高阶函数就可以把各种函数组合起来,函数接受函数作为参数然后返回一个新的函数,这个新的函数可以作为另一个函数
回调函数(callback)
把回调当做名词:被当做参数的函数就是回调(隐含条件: 回调函数要被call一下,这个被call的过程叫callback回调)
把回调当做动词:调用这个回调(调用这个被当做参数的函数)
注意回调跟异步没有任何关系
例子
array.sort(function(a,b){a-b})
上面的函数function(a,b){a-b}就是一个回调函数,因为它是另一个函数的参数。但是只有当它在被调用时(对数组进行排序)才叫回调callback
被调用(call)同时带回来值(back)才叫callback
例子:
// 1.异步的回调
setTimeout(fn,1000)
// 2. 同步的回调
array.sort(function(a,b){a-b})
有的时候异步的时候会用到回调,有的时候回调会用到异步,异步和回调只是2种现象同时出现的而已,异步和回调之间并不存在任何关系,彼此是独立存在的,就像是男医生和女医生『医生和男女之间并不存在什么关系』
函数里的各种名字
A函数接收函数作为参数,A函数叫高阶函数
A函数返回一个函数,A这个函数也叫高阶函数
A函数被当做参数,A就是回调函数
A函数返回的函数参数要比原函数少一个参数A就叫柯里化函数
返回对象的函数叫构造函数
函数式编程里的概念很多,但是可以用语言清晰的描述出来;但是面向对象里很多概念用文字都很难去定义,比如:封装、继承、多态
构造函数
返回对象的函数就是构造函数 ,一般首字母大写
如Number(1)、String(2)
自己写个构造函数
function Empty(){
return {}
}
一般不用自己写 return ,也不用写 “{}”
function Empty(){
}
var empty = new Empty() //相当于Empty.call({})
因为当我们写new语法糖的时候,会自动帮我们写几行代码
function Empty(){
return {} // new帮我们写好了
}
如果我们想要加属性怎么办? => 通过this
function Empty(){
this.name = 'lee'
return this // new帮我们写好了
}
this默认是一个空对象
当我们写var empty = new Empty()
相当于Empty.call({})
那么this = {}
也就是说,当我们用new调用构造函数的时候,new语法糖帮我们做了下面的事:
1.自动往构造函数里添加了this并且这个this就是"{}" 2.帮我们自动return这个this
总结:
1.new的去糖形式
// Empty就是构造函数
function Empty(){
this.name = 'lee'
return this
}
var empty = Empty.call({})
2.new的语法糖形式
function Empty(){
this.name = 'lee'
}
var empty = new Empty()
上面2种写法是等价的
箭头函数
箭头函数同普通函数的区别:箭头函数里没有自己的this (JS仿佛在说不要用this了,可能官方都觉得this不好用)
例1
setTimeout(function(){
console.log(this)
}.bind({name:'lee'}),1000)
// 1秒后输出 {name:'lee'}
上面标记的函数是"旧函数",旧的函数通过bind调用后返回新的函数fn,新的函数会调用旧的函数,调用旧的函数的时候旧的函数会被call 即function(){console.log(this)}.call({name:'lee'}),call的时候传的第一个参数是{name:'lee'},因此this就是call的第一个参数{name:'lee'}
例 2
setTimeout(function(a){ // 1处
console.log(this)
setTimeout(function(a){ // 2处
console.log(this)
},1000)
}.bind({name:'lee'}),1000)
// 1秒后输出 {name:'lee'}
// 2秒后输出 window
1处的this同2处的this是不一样的,就好比1处的参数a同2处的参数a是不一样的。2处的this就是call这个函数时传的第一个参数,同a一样也是参数,既然是参数就是动态的,被调用的时候传的什么就是什么,这个函数被调用的时候没有传this,因此这个this就是window
如何让2处的this也是{name:'lee'}呢? => 继续bind
setTimeout(function(a){ // 1处
console.log(this)
setTimeout(function(a){ // 2处
console.log(this)
}.bind(this),1000) // 3处
}.bind({name:'lee'}),1000)
3处的this就不用写{name:'lee'},因为此处的this还没有进入到函数里面,3处的this就是1处的this
3处的this:把函数外面的this当做函数里面的this
如上图所示旧的函数被bind以后会返回新的函数,新的函数被调用的时候,回去调用旧函数(在旧函数后面加call()),call()的第一个参数就是bind的第一个参数,所以这个旧函数被call的时候里面的this就是外面的this,因此bind就充当了函数里外this交换的媒介
接下来用箭头函数来重写就不需要bind了
setTimeout(function(a){ // 1处
console.log(this)
setTimeout(()=>console.log(this),1000) // 2处
}.bind({name:'lee'}),1000)
由于箭头函数内本身没有自己的this,因此它里面的2处this也就是外面的1处的this
箭头函数中的this就是个变量而已,自身找不到(因为自己没有this)就按照作用域的规则往上找,到它的父作用域找
当我们希望一个函数里面的this和它外面的this一样的时候,就用箭头函数,每次进入一个函数的时候,第一:会将位置记录到call stack里,不用指定this
而function本身一定有this,每次进入一个函数的时候,第一:会将位置记录到call stack里,第二:一定要强制的确定一个this
看一个奇怪的例子:
即使是用call的形式强制的指定this,也是被拒绝的,箭头函数里面的this就是外面的this,也就是window。箭头函数内的this就相当于一个变量,而function中的this是一个参数,它的值是要根据函数被call(asThis,asArg)的时候传的值来确定的。变量要遵循的是词法作用域,词法作用域的规则是:先从自己的作用域找,找不到就往它的父作用域找
拓展:函数参数的作用域问题
参数形成单独作用域
```
let x = 1;
function fun(x, y = x) {
console.log(y);
}
fun(2);
```
- 参数
y的默认值等于变量x - 调用函数
fun时,参数形成一个单独的作用域 - 在这个作用域中,默认值变量指向第一个参数
x,而不是全局环境的x
有默认值的形参创建的作用域也会沿着作用域链查找变量
function fun(y = x) {
let x = 2;
console.log(y);
}
fun(); // ReferenceError: x is not defined
- 调用函数
fun时,参数y=x形成一个单独的作用域 - 在这个作用域里,没有定义
x,所以沿着作用域链在全局寻找变量x - 由于全局环境中也没有定义变量
x,所以会报错 - 函数调用时,函数体内部的局部变量
x影响不到参数默认值变量x
避免暂时性死区(TDZ)
let x = 1;
function fun(x = x) {}
fun(); // Uncaught ReferenceError: x is not defined
- 参数
x = x形成一个单独作用域 - 在这个作用域中,执行的是
let x = x,这就是形成暂时性死区的原因
如果参数的默认值是一个函数,该函数的作用域也遵守上面的规则
let foo = "outer";
function bar(func = () => foo) {
let foo = "inner";
console.log(func());
}
bar(); // outer
- 函数
bar的参数func的默认值是一个匿名函数,返回值为变量foo - 形参形成的单独作用域里,并没有定义变量
foo,所以指向外层的全局变量foo