callStack调用栈
定义
调用栈其实就是一种解析器去处理程序的机制,它是栈数据结构。当执行环境中调用了多个函数函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。
原理
JS引擎在调用一个函数前,需要把函数所在的环境push到调用栈,等函数执行完了,就会被环境pop弹出,然后return到之前的环境,继续执行后续的代码。它能追踪子程序的运行状态。
- 每调用一个函数,解释器就会把该函数添加进调用栈并开始执行。
- 正在调用栈中执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行。
- 当前函数执行完毕后,解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。
- 当分配的调用栈空间被占满时,会引发“堆栈溢出”错误。
实例
- 代码
function boo (a) {
return a * 3
}
function foo (b) {
return boo(4) * 2
}
console.log(foo(3))
- 分析代码

- 首先调用
console.log(foo(3)),形成栈帧,放置于调用栈底部,也称为压栈,记录console.log(foo(3))函数执行后返回的位置。 - 然后调用
foo(3),形成栈帧,放置于console.log(foo(3))之上,也称为压栈,记录foo(3)函数执行后返回的位置。 - 接着调用
boo(4),形成栈帧,放置于foo(3)之上,也称为压栈,记录foo(4)函数执行后返回的位置。 - 当执行完
boo(4)时候,返回值给foo函数之后,boo(4)被推出调用栈,也叫弹栈,返回原来的位置。foo函数继续执行,然后foo函数执行完,被推出调用栈,返回值给console.log(foo(3))函数,console.log得到foo函数的返回值,运行,输出结果,最后console.log也被推出调用栈,该段程序执行完成。
factorial阶乘函数
定义
假设n为数字,阶乘是n前面所有数的乘积(包括n)。当n为1时,阶乘为1,当n不为1时,阶乘为n x (n-1)
代码实例
function f(n){
return n !== 1 ? n*f(n-1) : 1
}
recursion递归函数
定义
本质是一种函数调用自身的操作,递归被用于处理包含有更小的子问题的一类问题。一个递归函数可以接受两个输入参数:一个最终状态(终止递归)或一个递归状态(继续递归)。
代码实例
先递进,再回归
f(4)
= 4 * f(3)
= 4 * (3 * f(2))
= 4 * (3 * (2 * f(1)))
= 4 * (3 * (2 * (1)))
= 4 * (3 * (2))
= 4 * (6)
= 24
递归函数的调用栈
递归函数的调用栈很长,下面是阶乘4的调用栈,一共有4次压栈,4次弹栈。

function computeMaxCallStackSize(){
try{
return 1 + computeMaxCallStackSize();
} catch(e){
// 报错说明爆栈了,stack overflow
return 1
}
}
* Chrome 12578
* Firefox 26773
* Node 12536
callstack Overflow爆栈
如果调用栈中压入的帧过多,程序就会奔溃
function Hoisting函数提升
定义
不管把具名函数放在哪里,它都会跑到语句中的第一行
代码实例
在这里并不会报错,因add函数会跑到第一行,在add(1,2)的前面,申明的具名函数会提升,称为函数提升
add(1,2)
function add(x,y){
return x + y
}
反面实例1-报错的函数提升
因为let只准声明一次,所以add为变量数字,不能在被赋值,也不会被提升
let add = 1
function add(){} // 报错,因为add已经被申明了
反面实例2-变量提升
因为var可以被重复申明,同时var是全局变量,申明的时候会被提升,成为变量提升
function add(x,y){}
var add = 1
反面实例3-不是函数提升
因为左边的是赋值,右边的匿名函数申明不会提升,不能在函数未被申明的时候,调用函数,结果是会报错
add(1,2)
let add = function(x,y){return x + y}
arguments
arguments定义
由于JavaScript允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。arguments对象包含了函数运行时的所有参数。
arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
let f = function (one) {
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
}
f(1, 2, 3)
// 1
// 2
// 3
arguments是array-lik object伪数组
需要注意的是,虽然arguments很像数组,但它是一个对象,也叫做伪数组,拥有length属性,没有数组共有的属性,比如slice()和forEach(),不能在arguments对象上直接使用。
传递实际参数给arguments
在下面的代码中,fn(1,2,3)传递了三个实际参数给arguments,那么就是[1,2,3]的伪数组
function fn(){
console.log(arguments);
}
fn(1,2,3)
this定义
面向对象语言中 this 表示当前对象的一个引用,但在 JavaScript 中 this 不是固定不变的,它会随着执行环境的改变而改变
不同情况下的this指向
this指向window
不给任何条件,this默认指向window,window 就是该全局对象为[object Window]
function fn(){
console.log(this)
}
fn()
// winow object
this为undefined
在严格模式下,如果this没有被execution context执行环境定义,那它将保持为undefined。
function f2(){
"use strict"; // 这里是严格模式
return this;
}
f2() === undefined; // true
this指向所属对象
如果要想把this的值从一个环境传到另一个,就要用call() 或者apply()方法。
// 将一个对象作为call和apply的第一个参数,this会被绑定到这个对象。
var obj = {a: 'Custom'};
// 这个属性是在global对象定义的。
var a = 'Global';
function whatsThis(arg) {
return this.a; // this的值取决于函数的调用方式
}
whatsThis(); // 'Global' 指向全局对象
whatsThis.call(obj); // 'Custom' 指向所属对象
whatsThis.apply(obj); // 'Custom' 指向所属对象
this绑定特定对象
当一个函数在其主体中使用this 关键字时,可以通过使用函数继承自Function.prototype的 call()或 apply()方法将this 值绑定到调用中的特定对象。
function add(c, d) {
return this.a + this.b + c + d;
}
var o = {a: 1, b: 3};
// 第一个参数是作为‘this’使用的对象
// 后续参数作为参数传递给函数调用
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
// 第一个参数也是作为‘this’使用的对象
// 第二个参数是一个数组,数组里的元素用作函数调用中的参数
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34
假设没有this
代码实例一:引用变量
let person = {
name: 'frank',
sayHi(){
console.log(`你好,我叫`+ person.name)
}
}
person.sayHi()
这种方法称为引用,通过对象地址的变量来获取name
代码实例一的问题
let sayHi = function(){
console.log(`你好,我叫`+ person.name)
}
let person = {
name:‘frank,
'sayHi':sayHi
}
person.sayHi === ???
在上面的代码里面person如果改名,sayHi函数就挂了。同时,sayHi函数可能在另外一个文件里面,出现person指定不清楚的问题
代码实例二:使用class的问题
class Person{
constructor(name){
this.name = name
// 这里的this是new强制指定的
}
sayHi(){
console.log(???)
}
}
在上面的代码中,只有类,还没有创建对象,也不可能得到对象的name。存在逻辑上的矛盾,不能对未生成的事物,进行操作
代码实例二:使用class的解答
- 通过用
arguments传给对象,使用对象的name
let person = {
name: 'frank',
sayHi(p){
console.log(`你好,我叫` + p.name)
}
}
person.sayHi(person)
- 通过用
arguments传给类,使用类的name
class Person{
constructor(name){
this.name = name
}
sayHi(p){
console.log(`你好,我叫`+p.name)
}
}
代码实例三:python的解答
怎么样让类,对还没有出现的实例进行操作呢?
python的思路是新建对象person,通过赋值属性给这个新的person, 属性得到了保存,然后再从新对象这里调用。这样就完成立对未出现对象的操作。打个比方说,你的孩子还没出生,但是你有东西想给他,然后你把东西放在一个地方,等他出生了就可以给他了。在这里,孩子就是新的实例person,这个地方就是self
class Person:
def_init_(self, name): # 构造函数
self.name = name
def sayHi(self):
print('Hi,I am' + self.name)
person = Person('frank')
person.sayHi()
- 每个函数都接受一个额外的
self,这个self就是传进来的对象,等于person - 只不过
Python会偷偷的把你传递对象,person.sayHi() === person.sayHi(person)。这样,person就传给self
this的出现在JS中
用this获取未出现的对象
let person = {
name: 'frank',
sayHi(){
console.log(`你好,我叫` + this.name)
}
}
在这里,person.sayHi()相对于person.sayHi(person),然后person被传给this(person是个地址)。这样,每个函数都能用this获取一个未知对象的引用了。person.sayHi()会隐式的把person作为this传给sayHi,这样做就方便sayHi获取person对应的对象
小结
- 我们想让函数获取对象的引用,但是并不想通过变量名做到
python通过额外的self参数做到javaScript通过额外的this做到,this就是最终调取sayHi()的对象。
this的调用方法
- 第一种,
person.sayHi()会自动把person传到this里 - 第二种,
person.sayHi.call(person),需要手动把person传递函数里,作为this - 推荐第二种,深入学习运用,理解概念
call()方法
定义
call()方法可以用来在一个对象调用另一个对象的方法,也可以改变调用方法this的指向
function a(){
console.log(this);
}
a(); // this 默认指向window
a.call({name:"西瓜"}); // this指向传入的对象{name:"西瓜"}
语法
function.call(thisArg,arg1,arg2,...)
原理
手动来实现一个call()方法
Function.prototype.MyCall = function(obj){
var newObj = obj || window;
newObj.fn = this;
var params = [...arguments].slice(1);
var result = newObj.fn(...params);
delete newObj.fn;
return result;
}
- 首先定义一个新的对象,若传入对象的
obj存在,则新对象等于obj,若obj不存在,则等于window; - 然后把
this挂在到当前定义的新对象上(this即为调用的函数) - 第
4行代码得到了函数附带的参数 - 然后执行创建的新对象
newObj的fn函数 - 最后在执行了以后,把这个挂载的
fn函数删除 - 返回结果
result
问题
function test(){
console.log(this);
}
test();
test.MyCall({name:"西瓜"});
所以在这里 结果为
window{****}
{name:"西瓜"}
进阶问题
function f1(a){
console.log(1);
console.log(this);
}
function f2(){
console.log(2);
console.log(this);
}
f1.call(f2);
f1.call.call(f2);
答案为:
1
f2(){console.log(2);console.log(this);}
2
window{*****}
第二个结果返回为window对象是因为在这里,可以最终简化为f2.call(),没有传入对象,所以指向window
var newObj = f2;
f2.fn = Function.prototype.MyCall;
this还是指向f2()f1.call()就是Function.prototype.MyCall- 最终就是
f2()调用call()得出结果 f1.call.call(f2) === Function.prototype.call(f2) === f2.call()
apply()方法
call(),apply()方法区别是,从第二个参数起,call()方法参数将依次传递给借用的方法作参数,而apply()直接将这些参数放到一个数组中再传递, 最后借用方法的参数列表是一样的。
语法
function.apply(this,[argumentsArray])
手动来实现一个apply()方法
Function.prototype.newApply = function(context, parameter) {
if (typeof context === 'object') {
context = context || window
} else {
context = Object.create(null)
}
let fn = Symbol()
context[fn] = this
context[fn](parameter);
delete context[fn]
}
应用:
let array = ['a', 'b'];
let elements = [0, 1, 2];
array.push.apply(array, elements);
console.info(array); // ["a", "b", 0, 1, 2]
bind()方法
bind()也是函数的方法,作用也是改变this执行,同时也是能传多个参数。与call和apply不同的是bind方法不会立即执行,而是返回一个改变上下文this指向后的函数,原函数并没有被改变
bind() 方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入bind()方法的第一个参数作为 this,第二个以及以后的参数,加上绑定函数运行时本身的参数,按照顺序作为原函数的参数来调用原函数。
语法
function.bind(this,arg1,arg2,arg3,...)
手动实现bind()方法
Function.prototype.bind = function (context,...innerArgs) {
var me = this
return function (...finnalyArgs) {
return me.call(context,...innerArgs,...finnalyArgs)
}
}
应用:
let person = {
name: 'Abiel'
}
function sayHi(age,sex) {
console.log(this.name, age, sex);
}
let personSayHi = sayHi.bind(person, 25)
personSayHi('男')
实例
- 没有用到
this
function add(x,y){
return x + y
}
add.call(undefined,1,2)
// 3
上面的代码种,为什么要多写一个undefined是因为代码没有传入对象,用undefined或者null都可以
- 使用到
this
let array = [1,2,3]
Array.prototype.forEach2 = function(fn){
for(let i = 0; i < this.length; i++){
fn(this[i],i,this)
}
}
array.forEach2.call(array,(item)=>console.log(item))
this是什么? 这里的this指的的是array
this 一定是数组吗?
不一定,也可以是对象,例如array.forEach2.call({0:'a',1:'b'},(item)=>console.log(item))
this的两种使用方法
隐式传递
fn(1,2)其实等价于fn.call(undefined,1,2)
obj.child.fn(1)其实等价于obj.child.fn.call(obj.child,1)
显示传递
fn.call(undefined,1,2)
fn.apply(undefined,[1,2])
bind() 绑定this
- 使用
bind()可以让this不被改变
function f1(p1, p2){
console.log(this,p1,p2)
}
let f2 = f1.bind({name:'frank'}) // f2就是f1绑定之this之后的新函数
f2() // 等价于f1.call({name:'frank'})
- 使用
bind()还可以绑定其他参数
let f3 = f1.bind({name: 'frank')}, 'hi')
f3() // 等价于f1.call({name: 'frank'}, hi)
ArrowFunction箭头函数
箭头函数里面的this就是外面的this
let fn = () => console.log(this)
// window{****}
call()方法指定this也不起作用
let fn2 = () => console.log(this)
fn.call(2)
// window{****}
没有arguments
let fn3 = () => console.log(arguments)
fn3(1,2,3)
// 报错,arguments is not defined
立即执行函数
定义
IIFE(Immediately Invoked Function Expression ) 立即调用函数表达式是一个在定义时就会立即执行的JavaScript函数
(function () {
statements
})();
- 第一部分是包围在 圆括号运算符
()里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此IIFE中的变量,而且又不会污染全局作用域 - 第二部分再一次使用
()创建了一个立即执行函数表达式,JavaScript引擎到此将直接执行函数
原理
- 在
ES 5时代,为了得到局部变量,必须引入一个具名函数,这样做就多余了
var a = 1; // 申明全局变量a
function fn(){ // 申明全局函数,其中包含局部变量a
var a = 2
}
- 于是,这个函数必须是匿名函数,在函数后面加个
()执行这个函数
function (){
var a = 2
console.log(a)
}()
- 但是
JS认为这个语法不规范,于是程序员尝试了很多方法,发现匿名函数前面加个运算符就可以解决,!, ~, (), +, -都可运行,这样就可以形成局部作用域(推荐使用!运算符)。ES 6使用{}包括代码就可以解决。
! function (){
var a = 2
console.log(a)
}()
// 2
// true
以下情况需要加分号;
console.log('hi') //这里需要加;分号,。(推荐使用`!`运算符)
(function (){
var a = 2
console.log(a)
}())
因为console.log返回undefined,不加分号,下面的匿名函数就会往上顶,和上一句合并,变为console.log(undefined(function(){**})