this
1 全局环境中的this只想全局对象window
2 函数中的this对象,取决于函数是如何被调用的
简单调用=>window
对象方法调用=>对象本身
let obj = {
fn() {
console.log(this); //obj
function a() {
console.log(this); //window
}
a();
},
};
obj.fn();
3 call和apply调用 =>第一个参数时指定函数中的this对象
let obj = {
fn() {
console.log(this); //obj
function a() {
console.log(this); //obj
}
a.apply(this)//a.call(this)
},
};
obj.fn();
4 构造函数调用 => 指向构造函数
function Person() {
this.name = "tom";
}
let p = new Person();
console.log(p.name);
函数的双重职能
函数内部有两个不同的方法:[[Call]] 和[[Constructor]] ,使用普通方式调用时,会执行[[Call]] ,使用构造函数调用时,会执行[[Constructor]]
可以使用new.target来避免构造函数当成普通函数使用(instanceof也可以),当以构造函数的方式调用时,new.target指向的是构造函数本身
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error("u must use new with Person");
}
}
let p1 = new Person("tom");
let p2 = Person(); //Uncaught Error: u must use new with Perso
函数的传参方式
函数的参数,是按值传递(call by value),还是按引用传递(call by reference)
按值传递:函数形参的值是调用函数所传入实参的副本
按引用传递:函数形参的值是调用函数所传入实参的引用
基础数据类型
基础数据类型的传参方式是按值传递,这一点毫无疑问
let a = 1;
function fn(a) {
a = 2;
}
fn(a);
console.log(a); //1
引用数据类型
从下面这段代码看,引用类型似乎是按引用传递
let obj = {
x: 1,
};
function fn(obj) {
obj.y = 2;
}
fn(obj);
console.log(obj); //{x: 1, y: 2}
但是看下面的代码
let obj = { x: 1 };
function fn(obj) {
obj = 3;
}
fn(obj);
console.log(obj);// {x: 1}
其实,如果是引用类型的参数,传入的是对象引用的副本,他们引用的是同一个对象,第一段代码是在给实参和形参所引用的对象添加新的属性,第二段代码,是将引用地址的副本修改成了3
所以js中函数的参数是按值传递,这个话问题不大,但是有些人不认同这种说话,他们认为是按共享传参(call by sharing),这只是术语上的区别,直到具体是怎么个情况就可以了
函数应用
立即执行函数表达式,闭包,递归,回调,柯里化
立即执行函数表达式
创建了一个新的函数作用域,里面的局部变量和方法不会污染全局作用域
(function () {
let page = {
init() {
console.log("123");
},
};
page.init();
})();
ES6中,可以使用{}代替立即执行函数
{
let page = {
init() {
console.log("123");
},
};
page.init();
}
闭包
闭包:是指访问了另外一个作用域中的变量的函数
闭包的作用:阻止变量被垃圾回收
function a() {
var x = 1;
return function () {
return x + 1;
};
}
let b = a();
console.log(b()); //在运行完a函数之后,里面的变量x并没有被垃圾回收,应为,return出来匿名函数还对他有引用
闭包加立即执行函数的应用;封装(信息隐藏)
下面的代码虽然可以使用getName得到name ,但是也可以直接获取到name
let obj = {
name: "tom",
getName() {
return this.name;
},
};
console.log(obj.getName(), obj.name); //tom tom
下面的代码则可以做到让别人无妨访问私有属性
let obj = (function () {
let name = "tom";
return {
getName() {
return name;
},
};
})();
console.log(obj.getName());//tom
这在js中是一种很有用的技巧叫做模块模式,它可以把所有不必要暴露在外面的内容都封装在模块内部,对外只提供一些公开方法
递归:在函数中调用函数本身
function fib(n) {
if (n <= 2) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
fib(10);
回调 :同步回调 异步回调
同步回调
function a(n, func) {
++n;
func(n);
}
a(1, function (n) {
console.log(n);
});
柯里化函数
将使用多个参数的一个函数,转换成一系列使用一个参数的函数,并且返回接受余下参数且返回结果的函数
示例 实现生成唯一ID 的函数,要求
1 可以传入一个起始ID ,之后返回的id从这个值开始算。
2还可以传入一个数字 ,返回的值是:上次的id加上这个数字,如果没有传入数字,返回的结果返回上次的id+1
let fn = function (id) {
return function (num) {
return num ? id + num : id + 1;
};
};
let curry = fn("999");
console.log(curry(), curry(3)); //9991 9993
不难发现柯里化需要使用闭包的技术
除了上述的应用外,还有其他的应用,比如函数式编程
关于函数式编程的解释可以自行搜索
函数式编程
function not(fn) {
return function () {
let result = fn.apply(this, arguments);
return !result;
};
}
function even(x) {
return x % 2 === 0;
}
let odd = not(even);
console.log([1, 3, 5, 7].every(odd)); //true
箭头函数
箭头函数
箭头函数和普通函数的区别
1 没有自己的this,super,arguments和new.target,他们都是距离箭头函数最近的非箭头函数的绑定
2 不能使用new 来调用
3 没有原型对象
4 内部的this无法改变(就是说无法通过call,apply,bind来改变内部的this指向)
5 形参名称不能重复
尾调用优化
首先了解一下什么是尾调用
在执行某一个函数时,如果最后一步是调用一个另外一个函数,并且被调用的函数的返回值,直接被当前函数返回就是尾调用(tail call) 在了解尾调用之前需要了解没有尾调用优化是什么样子的,需要了解函数的执行过程,就拿下面的函数举例
function g(x) {
return x;
}
function f(x) {
const y = x + 1;
return g(y);
}
console.log(f(2));
初始化时,此时js引擎的栈中保存着这些信息,由于g在f之前声明,所以在栈的下面
当代码执行到第C行时,调用函数f ,会创建一个frame ,首先这个freame会要保存回到c行的地址,然后是分配f函数的参数,然后进入f函数的函数体,此时栈中的信息如下
当代码执行到b行时,同样会创建一个frame ,他会保存回到b行的地址,然后时分配给g参数,然后进入g的函数体,此时栈中的信息如下
当代码执行到a行时,他返回了x,之后就会移除函数g的frame ,然后回到第b行,
接下来就是把g返回的值,返回给f ,然后删除f的frame ,
最后代码执行C,输出3
在es6之前的函数调用就是这么执行的,可以发现,函数f的frame始终没有变化,他唯一的作用是记录回到C的信息
栈就是内存,是非常宝贵的,能销毁就要及时销毁,也就是说,创建函数g的frame的时候,就可以把函数f的frame销毁了,同时把回到c行的信息,保存到函数g的frame中去
所以尾调用优化,就是优化的栈的利用率
下面是尾调用的示意图,创建函数g的fream的同时,移除函数f的frame,并且把回到第c行的信息保存到g的frame中
尾调用要符合三个要求
1 尾调用不需要访问当前stack frame中的变量,也就是没有闭包
2 返回尾调用处时,不要做其他的事情
3 尾调用的放回值,直接返回给调用它的所在函数的调用者
尾递归
递归时,如果函数调用本身 时是一个尾调用,则称之为尾递归
下面代码不是尾递归,所以会堆栈溢出,造成网页卡死
function fib(n) {
if (n === 1 || n === 2) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
console.log(fib(100));//堆栈溢出
用下面这段代码就可以避免网页卡死
巧妙的把前两项之和通过a+b实现
function fib(n, a = 0, b = 1) {
if (n === 0) return 1;
if (n === 1) return b;
return fib(n - 1, b, a + b);
}
console.log(fib(100)) //354224848179262000000