JS进阶知识。
文章目录
目录
前言
这是函数的高级知识,主要涉及到JS原型链问题、执行上下文问题、作用域问题以及函数的闭包问题,是面试最常问到的JS考察点。
一、原型与原型链。
1.1 什么是原型(prototype)?
<title></title>
</head>
<!--
1、函数的prototype属性 ↑
*每个函数都有一个Prototype属性 它默认指向一个object 空对象(即为原型对象)_
*原型对象中有一个属性 constructor 它默认指向函数对象
2、给原型对象添加属性(一般都是作为方法添加)
*作用:所有的函数对象的实例对象自动拥有原型中的属性与方法;
-->
<body>
<script type="text/javascript">
console.log(Date.prototype)
function Fun1(){
}
console.log(Fun1.prototype)//默认指向一个object空对象 没有属性
console.log('________________________________')
//原型对象中有一个属性 constructor 它默认指向函数对象
console.log(Date.prototype.constructor === Date)
console.log(Fun1.prototype.constructor === Fun1)
//函数对象的实例对象自动拥有原型中的属性与方法;
Fun1.prototype.test = function (){
console.log('test()')
}
var fun1 = new Fun1();
fun1.test();
</script>
便于理解 关系如下图所示
其实为了理解原型链 我们首先得了解并承认下列最基础的结论:
我们继续。
1.2 什么是显式原型与隐式原型?
<title></title>
<!--
1.每个函数function 都有个prototype ,即为 显式原型(属性)
2、每个实例对象都有一个__proto__, 即为 隐式原型(属性)
3、对象的隐式原型的值 等于 其构造函数的显式原型的值。
***我们通过显式原型添加方法! 我们能直接通过显式原型直接操作它。
但是我们不能直接操作隐式原型!!(ES6之前 -我们正在学习ES5);
-->
</head>
<body>
<script type="text/javascript">
function Fn(){//故创建函数对象的内部语句就是 this.prototype = {}
}
//1、每个函数function 都有一个prototype 显式原型
console.log(Fn.prototype)
//2、每个实例对象都有一个__proto__, 即为 隐式原型(属性)
var fun1 = new Fn ()//由后面往前推我们可以知道————
//创建实例就是 this._proto__ = Fn.prototype 将原型函数的显示原型地址值赋值给了实例对象的隐式原型。
console.log(fun1.__proto__)
//3、对象的隐式原型的值 等于 其构造函数的显式原型的值。
console.log(Fn.prototype === fun1.__proto__)//返回一个true;
//两个都是引用类型的 变量保存的是 !地址值! 两个保存一样的地址 指向同一块 →原型对象;
</script>
! 注意 在Chrome等多数浏览器中 在控制台查看对象属性时 __proto__ 一般以[[prototype]]存在。
1.3 原型链:
<title></title>
<!--
-->
</head>
<body>
<script type="text/javascript">
function Fn (){
this.test1 = function(){
console.log('test1')
}
}
Fn.prototype.test2 = function(){
console.log('test2')
}
var fn = new Fn ()
fn.test1()
fn.test2()
console.log(fn.toString())
//fn的__proto__是FN.prototype 而后者的__proto__是 Object.prototype. 而再后者的__proto__找不到 返回undefined
/*原型链————在访问一个对象的属性时 现在自身的属性中查找 没找到再去__proto__链上往原型对象里找
如果最终找不到了 返回undefined(现在返回Null))别名*隐式原型链 作用 查找对象的属性与方法。
构造函数 与 原型 与实体对象的关系图解!
*/
console.log('______________________________________________')
console.log(Object.prototype.__proto__)
/*原型链补充总结:
1*函数的显式原型指向的对象默认是空的Object实例对象(但是Object不满足 它的显式原型对象是)
*/
console.log(Fn.prototype instanceof Object) //true
console.log(Object.prototype instanceof Object)//false 唯一例外
console.log(Function.prototype instanceof Object)//true
console.log('————————————————————————————————————————————————————————————————')
/* 2 Function 是它自身的实例。。 所有函数都是Function 的实例对象。
它是实例对象 有隐式原型↓ ↓ 它是自身的实例 有显式原型属性 */
console.log(Function.__proto__ === Function.prototype)//输出true
//Object的原型对象是原型链尽头
console.log(Object.prototype.__proto__ === null)//找不到它的原型了
</script>
更直观地 我们可以画出下列图 所有的原型链关系基本都可以在下图中找到对应关系:
1.4 原型链属性问题:
<title></title>
</head>
<body>
<script type="text/javascript">
function Pers(){
}
Pers.prototype.name = 'mqy'
var per1 = new Pers()
console.log(per1.name,per1)//**这里来到Pers.prototype找到属性name并输出
var per2 = new Pers()
per2.name = 'yyy'//这里是为 per2函数添加了name属性 值为yyy
console.log(per1.name)//仍然是‘mqy’没变 因为在自身per1里没有这个属性 就去原型链中找
console.log(per2.name)//改变为'yyy'了; 因为在per2中已经找到了这个属性 不去原型链中找了。
//总结:读取对象属性值时,会自动去原型链中查找,而再设置对象属性值时,不查找原型链 如果当前对象没有此属性 直接添加此属性
console.log('——————————————再来看看属性——————————————————————')
function Person (name,age){
this.name = name
this.age = age
}
Person.prototype.setName = function (name){
this.name = name
}
var person1 = new Person('wsl',20)
person1.setName('mls')
console.log(person1)//可以看到对象的属性值就直接存放在构造的对象里 而加入的函数仍然在原型对象中
var person2 = new Person('yrj',22)
person2.setName('kyx')
console.log(person2)
</script>
原型链的作用:
原型链:每一个对象,都有一个原型对象与之关联,这个原型对象它也是一个普通对象,这个普通对象也有自己的原型对象,这样层层递进,就形成了一个链条,这个链条就是原型链。通过原型链可以实现JS的继承,把父类的原型对象赋值给子类的原型,这样子类实例就可以访问父类原型上的方法了。
作用:
1.数据共享 节约内存内存空间。
2.实现方法属性等的继承。
二、执行上下文与上下文栈
2.1 变量提升与函数提升
变量提升即将变量声明提升到它所在作用域的最开始的部分。——一般通过var申明变量实现。
创建函数有两种形式,一种是函数声明,另外一种是函数字面量,只有函数声明才有变量提升
说明:函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被同名变量赋值后覆盖。
<title></title>
</head>
<!--
什么导致了变量提升?
js引擎执行前对代码的解析、
-->
<body>
<script type="text/javascript">
//试问a输出多少
var a = 3
function fn(){
console.log(a)//var定义的变量(此处为a)在定义语句之前都可以访问到 只不过值为undefined
var a = 4 //到这一步为a赋值 这就叫var的变量提升 值为undefined
}
fn()//a输出undefined
fn2()//可以调用下面的fn2定义 这就是函数提升 值就是函数对象
function fn2 (){
console.log('fn2')
}
//如果是var fn3 = funtion(){} 则产生的是变量提升而不是函数提升 。
</script>
2.2 执行上下文
执行上下文一般分为全局执行上下文和函数执行上下文。
全局执行上下文只有一个,它是由浏览器创建的,也就是常说的 window 对象。
函数执行上下文可能会有多个,当一个函数被调用时,就会产生一个函数执行上下文。当函数执行时,会创建一个称为执行期上下文的内部对象,函数执行就会产生执行期上下文(独一无二的),执行完一次会销毁一次。
具体实行如下所示:
<title></title>
</head>
<!-- 1、代码分类(位置)
*全局代码
*函数(局部代码)
2、全局执行上下文
*在执行全局代码前,将window确定为全局执行上下文
*对全局数据进行预处理 *var定义的全局变量 ==》undefined 并!添加!为window的属性
*function声明的全局函数 ==>赋值(fun),并添加为window的方法!
*this == 》 赋值为 window
*最后再开始执行全局代码。。。
3、在调用函数时,准备执行函数体之前,创建对应的函数执行上下文对象
*对局部数据进行预处理
*形参变量 == >赋值(实参) ==>添加为执行上下文的属性
*arguments ==>赋值(实参变量) ==> 添加为执行上下文的属性
*var 定义的局部变量 ==>undefined 添加为执行上下文的属性
*function声明的函数 ==>赋值 fun 添加为执行上下文的方法
*this ==> 赋值 (调用函数的对象)
*开始执行函数体代码。。
-->
<body>
<script type="text/javascript">
//全局执行上下文
console.log(a1,window.a1)
console.log(a2,window.a2)
console.log(this)
//既然可以访问这些 说明在代码开头js就已经执行了某些操作 window!
var a1 = 3
function a2(){
}
console.log('_____________________________________________')
//函数执行上下文
function fn (a1){
console.log(a1)//2
console.log(a2)//undefined
a3()//a3()
console.log(this)//window
console.log(arguments)//封装实参的伪数组(2,3)
var a2 = 3
function a3 (){
console.log('a3()')
}
}
fn(2,3)
</script>
2.3 执行上下文栈
JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)
来管理执行上下文
代码实行如下所示:
<title></title>
</head>
<!--
1.在全局代码执行前,js引擎就会创建一个栈来存储管理所有的执行上下文对象
2.在全局执行上下文(window)确定后 将其添加到栈中(压栈)
3.在函数执行上下文创建后 将其添加到栈中(压栈)
4.在当前函数执行完后,将栈顶的对象移除。(出栈)
5.当所有的代码执行完后 栈中只剩下window.
-->
<body>
<script type="text/javascript">
var a = 10//产生一个全局执行上下文对象window
var bar = function(x){
var b = 5
foo(x + b)//产生一个执行上下文对象
}
var foo = function(y){
var c = 5
console.log(a + c + y)
}
bar (10)//产生一个执行上下文对象 在函数调用的时候产生
//不会出错 这两个都是var 定义的函数 执行上下文对象产生次数 n+1 ___1是window n是函数调用次数。
/*执行上下文栈:
</script>
2.4 执行上下文栈 面试题实例:
<title></title>
</head>
<!--
1.依次输出什么?
2.整个过程中产生了几个执行上下文?
-->
<body>
<script type="text/javascript">
console.log('global begin:'+ i)//undefined
var i = 1
foo(1)
function foo(i){
if(i==4){
return;//递归终止
}
console.log('foo( ) begin:' + i) //fb=1 ;fb=2 fb = 3
foo(i + 1)//递归调用自身 f1 f2 f3依次压入栈
console.log('foo() end:'+ i)//结束之后开始出栈 fe = 3 fe = 2 fe = 1
}
console.log('global end:' + i)//i=1;
三、作用域与执行上下文
<title></title>
<!--
1.理解**就是一块地盘 一个代码块所在的区域 它是静态的(相对于上下文对象),在编写代码时就确定了
2.分类**全局作用域
**函数作用域
**块作用域(仅在ES6后产生)
3.作用及:*隔离变量,不同作用域下的同名变量不会有冲突。
-->
</head>
<body>
<script type="text/javascript">
var a = 10//作用域数 也是 n+1 (n个定义函数作用域 和 一个全局作用域。)
var b = 20
function fn(x){
var a = 100,c = 300
console.log('fn()',a,b,c,x)
function bar(x){
var a= 1000,d = 400
console.log('bar()',a,b,c,d,x)//其实就是就近原则 从函数作用域往全局作用域从里往外找。
}
bar(100)
bar(200)
}
fn(10)
</script>
执行上下文:
<title></title>
<!--
1.区别1
*全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了,而不是在函数调用时。
*全局执行上下文环境是在全局作用域确定之后,js代码马上执行之前创建
*函数执行上下文是在调用函数时产生,在函数体代码执行之前创建
2.区别2
*作用域是静态的,只要函数定义好了就一直存在,且不会再变化
*上下文环境是动态的,调用函数时创建,函数调用结束时,上下文环境就会被释放。
3.联系
*上下文环境(对象)是从属于所在的作用域
*全局上下文环境 ==> 全局作用域
*函数上下文环境 ==> 对应的函数使用域。
-->
</head>
<body>
<script type="text/javascript">
</script>
面试题实例:
<title></title>
<!--
-->
</head>
<body>
<script type="text/javascript">
var x = 10
function fn(){
console.log(x)
}
function show(f){
var x = 20
f()
}
show(fn)//输出10 函数的作用域写代码时就确定了 fn内部找不到x 就在全局作用域找 x=10; 输出10
console.log("_______________________________________")
var obj = {
fn2:function(){
// console.log(fn2) 这样找不到fn2 因为函数作用域里没有 往外找的全局作用域也没有
console.log(this.fn2)//这样可以找到这个fn2 因为这个fn2算是this.的属性。
}
}
obj.fn2()//fn2 is not defined
</script>
四、闭包
4.0 闭包的引入
通过下面这个案例可以看出闭包是如何工作 生效的
<title></title>
<!--
-->
</head>
<body>
<button type="button">测试1</button>
<button type="button">测试2</button>
<button type="button">测试3</button>
<script type="text/javascript">
var btns = document.getElementsByTagName('button')
//遍历加监听每个按钮
// var length = btns.length
// for(var i = 0; i <length; i++){
// var btn = btns[i]
// btn.index = i //如果没有这句话
// btn.onclick = function(){
// alert('第'+( i+1)+'个')//输出三个4 说明在函数执行的时候 i在几种情况下都为3
// }// ↑把这个改成this.index既可以正常输出 1 2 3
// }
// console.log(i)
for(var i =0,length = btns.length;i < length ; i++){
(function(i){
var btn = btns[i]
btn.onclick = function(){
alert('第'+( i+1)+'个')
}
})(i)//这些代码也可以正常执行 这就是 闭 包 的工作。
}
</script>
4.1 闭包的原理? 如何理解
<title></title>
<!--
1、如何产生闭包? ——*当一个嵌套的内部子函数引用了嵌套外部夫函数的变量(函数)时,就产生了闭包
2、闭包到底是什么?
*用chrome 调试查看 理解一:闭包是嵌套的内部函数(绝大部分人)
理解二:包含被引用变量的那个对象Z(少数人) 注意:闭包存在于嵌套的内部函数中。
3.产生闭包的条件?
*函数嵌套
*内部函数引用了外部函数的数据(变量/函数)
-->
</head>
<body>
<script type="text/javascript">
function fn1 (){
var a = 2
var b = 3
function fn2 (){//执行函数定义 不用执行内部函数fn2 就可以产生函数定义进而产生闭包(需要调用外部函数以定义内部)
//这里把函数定义改为 var fun2 = function(){}就不会产生闭包 因为这里是对fun2变量提升 没有定义函数。
console.log(a)
}
fn2()
}
fn1()
</script>
下面就是一些常见的闭包的实例 可以加深我们的理解 显得这个概念不是那么抽象。
<title></title>
<!--
-->
</head>
<body>
<script type="text/javascript">
//情况一 将函数作为另一个函数的返回值
function fn1(){
var a = 2
function fn2(){
a++
console.log(a)
}
return fn2
}
var f = fn1()
f()//3
f()//4 两次一共只创建了一个内部函数fn2 故总共只产生了一个闭包。
fn1()//又创建一个闭包对象——外部函数又执行一次 内部函数对象被创建
//而跟内部函数被执行的次数没有关系。
console.log('______________________________________________')
//情况二 将函数作为实参传递给另一个函数调用
function showDelay(msg,time){//有外部函数 有内部函数 内部函数里引用了外部函数的msg 没有time 故产生了一个只包含msg 的闭包对象。
setTimeout(function(){
alert(msg)
},time)
}
showDelay('mmmsn',2000)
</script>
4.2 闭包的作用:
<title></title>
<!--
1.使用函数内部的变量 在函数执行完后 仍然可以存活在内存中(延长了局部变量的生命周期)
2.让函数外部可以操作(读写)到函数内部的数据(变量/函数)
问题:
1.函数执行完后,函数内部声明的局部变量是否还存在?
一般不存在,,,但是有闭包的话存在于闭包之中。
2.在函数外部能直接访问到函数内部的局部变量吗?
访问不到。
-->
</head>
<body>
<script type="text/javascript">
//情况一 将函数作为另一个函数的返回值
function fn1(){
var a = 2
function fn2(){
a++//执行完之后a还存在 于闭包之中
console.log(a)
}
function fn3(){//fn3 在fn1执行完之后会被释放 因为他没有被引用 被存于闭包之中
//但是它没有成为垃圾对象 因为 后面的f存起来了fn1的返回值 保存着fn3的地址。 因为有f指向着fn3 所以fn3被引用 不会成为垃圾对象 。
a--
console.log(a)
}
return fn3
}
var f = fn1()
f()//3
f()//4 两次一共只创建了一个内部函数fn2 故总共只产生了一个闭包。
fn1()//又创建一个闭包对象——外部函数又执行一次 内部函数对象被创建
//而跟内部函数被执行的次数没有关系。
console.log('______________________________________________')
//情况二 将函数作为实参传递给另一个函数调用
// function showDelay(msg,time){//有外部函数 有内部函数 内部函数里引用了外部函数的msg 没有time 故产生了一个只包含msg 的闭包对象。
// setTimeout(function(){
// alert(msg)
// },time)
// }
// showDelay('mmmsn',2000)
</script>
4.3 闭包的生命周期
<title></title>
<!--
1.产生:在调用外部函数时 在嵌套的内部函数定义执行完时 就产生了(不是在调用内部函数时)
2.死亡:在嵌套的内部函数变为垃圾对象时
-->
</head>
<body>
<script type="text/javascript">
function fn1(){
//在这一步 进入fn1时 fn2的定义就完成了 fn2中对于a的闭包已经产生
var a = 2
function fn2(){
a++
console.log(a)
}
return fn2
}
var f = fn1()
f()//3
f()//4
//加上下面这一步 使得 fn2不再有人包含这个闭包对象 没有变量再去引用这个闭包对象。
// f = null
</script>
4.4 闭包的应用——自定义JS模块
<title></title>
<!--
自定义js模块:*具有特定功能的js文件
*将所有的数据和功能都封装在一个函数内部(私有的)
-->
</head>
<body>
<!-- <script type="text/javascript" src="testModule.js">
</script>
<script type="text/javascript">
var funtest = Test()
funtest.doAny()
funtest.doSome()
//这就用到了闭包——
</script> -->
<script type="text/javascript" src="testModule2.js">
</script>
<script type="text/javascript">
testModule2.doAny()//也利用了闭包 将方法添加到了window中
</script>
闭包的缺点及其解决:
<title></title>
<!--
1.缺点
*执行完函数后,函数内的局部变量没有释放,占用内存的时间会边长。
*容易造成内存泄露。
2.解决
*能不用闭包就尽量不用
*及时释放内存 。
-->
</head>
<body>
<script type="text/javascript">
function fn1 (){
var arr = new Array[100000]
function fn2(){
console.log(arr.length)
}
return fn2
}
var f = fn1()//产生闭包了 那个数组arr还存在 内存还在占用
f()
f=null //让内部函数成为垃圾对象 使得浏览器回收掉闭包对象 释放内存
</script>
4.5 补充——内存溢出与内存泄漏
</head>
<!--
1.内存溢出
*一种程序运行出现的错误
*当程序运行需要的内存超过了剩余的内存时,就会出现内存溢出的错误
2.内存泄漏
*占用的内存没有得到及时释放
*内存泄漏积累多了就容易导致内存溢出
*常见的内存泄漏:
*意外的全局变量
*没有及时清理的计时器或回调函数
*闭包对象。
-->
<body>
<script type="text/javascript">
// //1、内存溢出
// var obj = {}
// for(var i = 0 ; i<10000 ; i++){
// obj[i] = new Array(10000000000)//内存溢出 浏览器崩溃 无法运行
// }
// 意外的全局变量
function fn(){
a = 3 //这是一个全局变量 因为它是在全局中定义的 只是在局部里赋值而已
console.log(a)
}
fn()//所以在函数运行完之后 a并没有被释放 这就造成了意外的全局变量。
var int1 = setInterval(function(){//启动循环定时器后不清理 不断执行
console.log("___________________")
},1000)
clearInterval(int1)//需要用这个清除掉定时器 否则会不断循环 申请内存
function fn1(){
var a = 4
function fn2 (){
console.log(++a)
}
return fn2
}
var f = fn1()
f()
f = null //需要这一代码将闭包对象清理掉。
</script>
4.6 闭包面试题实例
第一题:
<title></title>
</head>
<body>
<script type="text/javascript">
//情况一
var name = 'the window'
var object = {
name:'my object',
getNameFunc:function(){
return function(){
return this.name ;
}//有函数嵌套 没有内部函数引用外部变量 没有闭包
}
}
alert(object.getNameFunc()())//返回 the window
//object.getNameFunc() 执行的是一个函数 function(){return this.name} 此函数直接执行
//故而this指的是window this.name 是全局变量中的name
//情况二
var name2 = 'the window'
var object2 = {
name2 : "my object2",
getNameFunc: function(){
var that = this
return function(){
return that.name2//有函数嵌套 有内部函数引用外部变量 有闭包
}
}
}
alert(object2.getNameFunc()())//返回 my object2
//object2.getNameFunc() 同样是直接执行函数 这里的this同样是window 但是这里的that 是被object里的this赋值了的 。
</script>
第二题:
<script type="text/javascript">
function fun(n,o){
console.log(o)
return{
fun:function(m){
return fun(m,n)
}
}
}
var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3)
console.log("————————————————————————————————————————————————————————————————————")
var b = fun(0).fun(1).fun(2).fun(3)
console.log("————————————————————————————————————————————————————————————————————")
var c = fun(0).fun(1)
c.fun(2)
c.fun(3)
</script>
总结
函数高阶知识点 深入理解JS的底层机制重点就在此。