js-函数(关于函数的那些事儿)
一,JavaScript代码解释执行的过程
JS 运行过程分三步:
语法分析(通篇扫描是否有语法错误),预编译(发生在函数执行的前一刻),解释执行(一行行执行)。
预编译阶段干了什么???
预编译分为四步:
1、创建AO对象
就是隐式的创建一个AO(Activation Object)空对象。( 执行期上下文)
2、查找形参和变量声明,将形参名和变量名作为AO对象的属性,值为undefined
注意此处是变量声明(必须带var),只是查找形参名和变量声明名不赋值。
3、将形参与实参统计
即修改AO对象中属性名为形参的值为传入的实参,如果没有形参此步略过。
4、查找函数声明,函数名作为AO对象的属性,函数体作为的值
此处是函数声明,而非匿名函数和函数表达式。
二,变量提升与函数提升
1、变量提升
- 通过var定义(声明)的变量, 在定义语句之前就可以访问到
- 值: undefined
2、 函数提升
- 通过function声明的函数, 在之前就可以直接调用
- 值: 函数定义(对象)
3、 提升注意点
- 函数的提升是整体的提升
- 变量的提升是声明的提升
- 函数的提升优于变量的提升
- 提升是指提升到本层作用域的最顶层
4、注意事项和使用细节
- 变量的提升不会理会if else 这种条件暗示,会跳出流程,进行提示。
- 永远不要再流程控制语句的块内部定义函数
console.log(a)//? a();//? var a =3; function a(){ console.log(10); } console.log(a);//? a = 6; a();//?
结果为:
function a(){ console.log(10); } var a;//再次申明a,并未修改a的值,忽略此处申明 console.log(a)//输出函数本体 a();//函数申明提前,可调用,输出10 a =3;//这里修改值了,a=3,函数已不存在 console.log(a);//输出3 a = 6;//再次修改为6,函数已不存在 a();//a已经为6,没有函数所以没法调用,直接报错
三,函数执行上下文
为什么调用函数的时候要创建一个执行上下文?因为JavaScript要保存一些函数调用时的信息,比如传入的参数、谁调用的。执行上下文是由JavaScript的一个内部实现,类似于一个简单的对象,这个对象拥有三个属性:变量对象,作用域链,this。
定义:执行上下文以一套非常严格的规则,规范变量存储、提升、this指向。
分类: 全局执行上下文;函数执行上下文
作用:对数据进行预处理
如何计算执行上下文环境的个数?
- n+1
- n:函数调用的次数,函数每调用一次,开启一个对全局数
- 1:全局上下文环境
执行上下文栈:
1、 理解压栈
- 当全局代码开始执行前,先创建全局执行上下文环境
- 当全局执行上下文环境创建好了以后将上下文中的所有内容放入栈内存
- 最先放入的在最下边(global)
- 其他执行的函数的执行上下文依次放入(放入的顺序是代码的执行顺序)
- 栈中最后放入的执行完最先出栈。
四,作用域与作用域链
作用域:
作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域***的用处就是隔离变量,不同作用域下同名变量不会有冲突。
作用域链:
如下图所示,通过多层嵌套函数设计一个作用域链,在最内层函数中可以逐级访问外层函数的私有变量,JavaScript 引擎首先在最内层活动对象中查询属性 a、b、c 和 d,从中只找到了属性 d,并获得它的值(4);然后沿着作用域链,在上一层活动对象中继续查找属性 a、b 和 c,从中找到了属性 c,获取它的值(3)······以此类推,直到找到所有需要的变量值为止,这就是作用域链
五,作用域和执行上下文
区别1:
1. 除全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时。
2. 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建。
3. 函数执行上下文环境是在调用函数时, 函数体代码执行之前创建。
总结:作用域是在代码编译时确定的, 全局执行上下文环境在代码执行前确定, 函数执行上下文环境在函数体代码执行前创建。
区别2:
1. 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
2. 上下文环境是动态的, 调用函数时创建, 函数调用结束时上下文环境就会被释放
总结:
作用域是静态的,上下文环境是动态的。
执行上下文是附属于自己对应的作用域的。
区别3:
一个作用域 在一个时刻可以有多个上下文,处于活动状态的上下文只有一个。
联系
1. 上下文环境(对象)是从属于所在的作用域
2. 全局上下文环境==>全局作用域
3. 函数上下文环境==>对应的函数使用域
六,原型与原型链
要点:
1、所有的引用类型(数组、函数、对象)可以自由扩展属性(除null以外)。
2、所有的引用类型都有一个’__proto__'属性(也叫隐式原型,它是一个普通的对象)。
3、所有的函数都有一个’prototype’属性(这也叫显式原型,它也是一个普通的对象)。
4、所有引用类型,它的’__proto__'属性指向它的构造函数的’prototype’属性。(一定要记住)
5、当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,那么就会去它的’__proto__'属性(也就是它的构造函数的’prototype’属性)中去寻找
原型:
<script> //这是一个构造函数 function Foo(name,age){ this.name=name; this.age=age; } /*根据要点3,所有的函数都有一个prototype属性,这个属性是一个对象 再根据要点1,所有的对象可以自由扩展属性于是就有了以下写法*/ Foo.prototype={ // prototype对象里面又有其他的属性 showName:function(){ console.log(this.name); //this是什么要看执行的时候谁调用了这个函数 }, showAge:function(){ console.log(this.age); //this是什么要看执行的时候谁调用了这个函数 } } var fn=new Foo('小王',22) /*当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,那么就会去它构造函数的'prototype'属性中去找*/ fn.showName(); // 小王 fn.showAge(); // 22 </script>
这就是原型,通过原型,只需要在构造函数里面给属性赋值,而把方法写在Foo.prototype属性(这个属性是唯一的)里面。这样每个对象都可以使用prototype属性里面的showName、showAge方法,并且节省了不少的资源。
原型链:
根据要点5,当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,那么就会去它构造函数的’prototype’属性中去寻找。那又因为’prototype’属性是一个对象,所以它也有一个’__proto__'属性。
<script> // 构造函数 function Foo(name,age){ this.name=name; this.age=age; } //在Object对象的原型上定义了一个方法toString Object.prototype.toString=function(){ console.log("姓名:"+this.name+" 年龄:"+this.age);//this是什么要看执行的时候谁调用了这个函数。 } var fn=new Foo('小明',19); fn.toString(); //姓名:小明 年龄:19 console.log(fn.toString===Foo.prototype.__proto__.toString); //true console.log(fn.__proto__ ===Foo.prototype)//true console.log(Foo.prototype.__proto__===Object.prototype)//true console.log(Object.prototype.__proto__===null)//true </script>
分析:
首先,fn的构造函数是Foo()。所以: fn.__ proto __=== Foo.prototype
又因为Foo.prototype是一个普通的对象,它的构造函数是Object,所以: Foo.prototype.__proto__=== Object.prototype
通过上面的代码,我们知道这个toString()方法是在Object.prototype里面的,当调用这个对象的本身并不存在的方法时,它会一层一层地往上去找,一直到null为止。
所以当fn调用toString()时,JS发现fn中没有这个方法,于是它就去Foo.prototype中去找,发现还是没有这个方法,然后就去Object.prototype中去找,找到了,就调用Object.prototype中的toString()方法。
这就是原型链,fn能够调用Object.prototype中的方法正是因为存在原型链的机制。
另外,在使用原型的时候,一般推荐将需要扩展的方法写在构造函数的prototype属性中,避免写在__proto__属性里面。
注意:原型链属性问题
1. 读取对象的属性值时: 会自动到原型链中查找
2. 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
3. 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上
例子:
<script> function Fn() { } Fn.prototype.a = 'xxx' var fn1 = new Fn() console.log(fn1.a, fn1) var fn2 = new Fn() fn2.a = 'yyy' console.log(fn1.a, fn2.a, fn2) function Person(name, age) { this.name = name this.age = age } Person.prototype.setName = function (name) { this.name = name } var p1 = new Person('Tom', 12) p1.setName('Bob') console.log(p1) var p2 = new Person('Jack', 12) p2.setName('Cat') console.log(p2) console.log(p1.__proto__===p2.__proto__) // true </script>
七,闭包
什么是闭包?
有权访问另一个函数作用域内变量的函数都是闭包
如何产生闭包?
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包
闭包的作用
1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
缺点
1. 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
2. 容易造成内存泄露
详细解释:
当函数执行时,会创建一个称为**执行期上下文的内部对象(AO)**,执行期上下文定义了一个函数执行时的环境。
函数还会获得它所在作用域的**作用域链**,是存储函数能够访问的所有执行期上下文对象的集合,即这个函数中能够访问到的东西都是沿着作用域链向上查找直到全局作用域。
函数每次执行时对应的执行期上下文都是独一无二的,当函数执行完毕,函数都会失去对这个作用域链的引用,JS 的垃圾回收机制是采用引用计数策略,如果一块内存不再被引用了那么这块内存就会被释放。
但是,当闭包存在时,即内部函数保留了对外部变量的引用时,这个作用域链就不会被销毁,此时内部函数依旧可以访问其所在的外部函数的变量,这就是闭包。
例子:
/* 闭包经典例子 */ function fn() { var num = 3 return function() { var n = 0 console.log(++n) console.log(++num) } } var fn1 = fn() fn1() // 1 4 fn1() // 1 5 /* 循环中赋值为引用的问题 */ for (var i = 1; i < 5; i++) { setTimeout(function timer() { console.log(i); }, i * 1000); //解决办法: //一,使用`立即执行函数`方式 for (var i = 1; i < 5; i++) { (function(i){ setTimeout(()=>{console.log(i)}, i * 1000); })(i) } //二,使用 ES6 的`let` for (let i = 1; i < 5; i++) { setTimeout(()=>{console.log(i)}, i * 1000); } //三,使用setTimeout第三个参数 //setTimeout(function, milliseconds, param1, param2, ...),后面的参数传给function for (var i = 1; i < 5; i++) { setTimeout((j)=>{console.log(j)}, i * 1000,i); //将每次的i当作参数传给function }
八,JS 中,调用函数有哪几种方式?
- 方法调用模式 Foo.foo(arg1, arg2);
- 函数调用模式 foo(arg1, arg2);
- 构造器调用模式 (new Foo())(arg1, arg2);
- call/apply 调用模式 Foo.foo.call(that, arg1, arg2);
- bind 调用模式 Foo.foo.bind(that)(arg1, arg2)();
九,构造函数,new时发生了什么?
1. 创建一个新的对象 obj;
2. 将这个空对象的__proto__成员指向了构造函数对象 prototype 成员对象
3. 构造函数对象的 this 指针替换成 新对象obj, 相当于执行了 Base.call(obj);
4. 如果构造函数显示的返回一个对象,那么则这个实例为这个返回的对象。 否则返回这个新创建的对象
注意要点4:
如果构造函数内部有return
语句,而且return
后面跟着一个对象,new命令会返回return语句指定的对象;否择,就不会管return语句,返回this对象
例子:
<script> function Fn(name) { this.name = name return 'xixi' //return 为非对象,new命令会忽略这个return语句 } function Fn1(name) { this.name = name return {name:'xixi'} //return 为对象,返回return指定的对象 } var f = new Fn("haha") var f1 = new Fn1("haha") console.log(f) console.log(f1) </script>
那么,如何实现一个new?
<script> //实现一个new function _new(func, ...args) { let obj = {}; obj.__proto__ = func.prototype; // 原型 相当于 let obj = Object.create(func.prototype); let res = func.apply(obj, args); // 初始化对象属性 改变this指向 return res instanceof Object ? res : obj; // 返回值 } //测试 function Fn(name){ this.name = name } Fn.prototype.showName = function(){ console.log(this.name) } let f = _new(Fn,"小王"); f.showName(); //小王 </script>
补充:Object.create
- 语法:
Object.create(proto, [propertiesObject])
//方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。 - 参数:
- proto : 必须。表示新建对象的原型对象,即该参数会被赋值到目标对象(即新对象,或说是最后返回的对象)的原型上。该参数可以是
null
,对象
, 函数的prototype属性
(创建空的对象时需传null , 否则会抛出TypeError
异常)。 - propertiesObject : 可选。 添加到新创建对象的可枚举属性(即其自身的属性,而不是原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应
Object.defineProperties()
的第二个参数。
3 返回值:
在指定原型对象上添加新属性后的对象。
<script> function test() { this.name = name; }; test.prototype.showName = function(){ console.log(this.name) } let ff = Object.create(test.prototype, {age: { value: "28" }}); console.log(ff); </script>
十,函数参数是对象会发生什么问题?
<script> function test(person) { person.age = 26; person = { name: "yyy", age: 30, }; return person; } const p1 = { name: "hy", age: 25, }; const p2 = test(p1); console.log(p1); // ->??? console.log(p2); // ->??? </script>
答案是什么呢?
看这张图你就懂了