1.原型与原型链
1.原型(prototype)
- 函数的prototype属性(图)
- 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)
- 原型对象中有一个属性constructor, 它指向函数对象
- 给原型对象添加属性(一般都是方法)
- 作用: 函数的所有实例对象自动拥有原型中的属性(方法)
// 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)console.log(Date.prototype, typeof Date.prototype)function Fun () {//alt + shift +r(重命名rename)}console.log(Fun.prototype) // 默认指向一个Object空对象(没有我们的属性)// 原型对象中有一个属性constructor, 它指向函数对象console.log(Date.prototype.constructor===Date)console.log(Fun.prototype.constructor===Fun)//给原型对象添加属性(一般是方法) ===>实例对象可以访问Fun.prototype.test = function () {console.log('test()')}var fun = new Fun()fun.test()
2.显示原型与隐示原型
- 每个函数function都有一个prototype,即显式原型(属性)
- 每个实例对象都有一个proto,可称为隐式原型(属性)
- 对象的隐式原型的值为其对应构造函数的显式原型的值
- 内存结构(图)
- 总结:
- 函数的prototype属性: 在定义函数时自动添加的, 默认值是一个空Object对象
- 对象的proto属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值
- 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)
//定义构造函数function Fn() { // 内部语句: this.prototype = {}}// 1. 每个函数function都有一个prototype,即显式原型属性, 默认指向一个空的Object对象console.log(Fn.prototype)// 2. 每个实例对象都有一个__proto__,可称为隐式原型//创建实例对象var fn = new Fn() // 内部语句: this.__proto__ = Fn.prototypeconsole.log(fn.__proto__)// 3. 对象的隐式原型的值为其对应构造函数的显式原型的值console.log(Fn.prototype===fn.__proto__) // true//给原型添加方法Fn.prototype.test = function () {console.log('test()')}//通过实例调用原型的方法fn.test()
3.原型链
- 原型链(图解)
- 访问一个对象的属性时,
- 先在自身属性中查找,找到返回
- 如果没有, 再沿着proto这条链向上查找, 找到返回
- 如果最终没找到, 返回undefined
- 别名: 隐式原型链
- 作用: 查找对象的属性(方法)
- 构造函数/原型/实体对象的关系(图解)
- 构造函数/原型/实体对象的关系2(图解)
// console.log(Object)//console.log(Object.prototype)console.log(Object.prototype.__proto__)function Fn() {this.test1 = function () {console.log('test1()')}}console.log(Fn.prototype)Fn.prototype.test2 = function () {console.log('test2()')}var fn = new Fn()fn.test1()fn.test2()console.log(fn.toString())console.log(fn.test3)// fn.test3()/*1. 函数的显示原型指向的对象默认是空Object实例对象(但Object不满足)*/console.log(Fn.prototype instanceof Object) // trueconsole.log(Object.prototype instanceof Object) // falseconsole.log(Function.prototype instanceof Object) // true/*2. 所有函数都是Function的实例(包含Function)*/console.log(Function.__proto__===Function.prototype)/*3. Object的原型对象是原型链尽头*/console.log(Object.prototype.__proto__) // null
4,原型链属性问题
- 读取对象的属性值时: 会自动到原型链中查找
- 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
- 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上
unction 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 = namethis.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
5.探索instanceof
- instanceof是如何判断的?
- 表达式: A instanceof B
- 如果B函数的显式原型对象在A对象的原型链上, 返回true, 否则返回false
- Function是通过new自己产生的实例
/*案例1*/function Foo() { }var f1 = new Foo()console.log(f1 instanceof Foo) // trueconsole.log(f1 instanceof Object) // true/*案例2*/console.log(Object instanceof Function) // trueconsole.log(Object instanceof Object) // trueconsole.log(Function instanceof Function) // trueconsole.log(Function instanceof Object) // truefunction Foo() {}console.log(Object instanceof Foo) // false
6.面试题
/*测试题1*/function A () {}A.prototype.n = 1var b = new A()A.prototype = {n: 2,m: 3}var c = new A()console.log(b.n, b.m, c.n, c.m)/*测试题2*/function F (){}Object.prototype.a = function(){console.log('a()')}Function.prototype.b = function(){console.log('b()')}var f = new F()f.a()// f.b()F.a()F.b()console.log(f)console.log(Object.prototype)console.log(Function.prototype)
2.执行上下文与执行上下文栈
1.变量提升与函数提升
- 变量声明提升
- 通过var定义(声明)的变量, 在定义语句之前就可以访问到
- 值: undefined
- 函数声明提升
- 通过function声明的函数, 在之前就可以直接调用
- 值: 函数定义(对象)
- 问题: 变量提升和函数提升是如何产生的?
/*面试题 : 输出 undefined*/var a = 3function fn () {console.log(a)var a = 4}fn()console.log(b) //undefined 变量提升fn2() //可调用 函数提升// fn3() //不能 变量提升var b = 3function fn2() {console.log('fn2()')}var fn3 = function () {console.log('fn3()')}
2.执行上下文
- 代码分类(位置)
- 全局代码
- 函数(局部)代码
- 全局执行上下文
- 在执行全局代码前将window确定为全局执行上下文
- 对全局数据进行预处理
- var定义的全局变量==>undefined, 添加为window的属性
- function声明的全局函数==>赋值(fun), 添加为window的方法
- this==>赋值(window)
- 开始执行全局代码
- 函数执行上下文
- 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
- 对局部数据进行预处理
- 形参变量==>赋值(实参)==>添加为执行上下文的属性
- arguments==>赋值(实参列表), 添加为执行上下文的属性
- var定义的局部变量==>undefined, 添加为执行上下文的属性
- function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
- this==>赋值(调用函数的对象)
- 开始执行函数体代码
console.log(a1, window.a1)window.a2()console.log(this)var a1 = 3function a2() {console.log('a2()')}console.log(a1)
3.执行上下文栈
- 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
- 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
- 在函数执行上下文创建后, 将其添加到栈中(压栈)
- 在当前函数执行完后,将栈顶的对象移除(出栈)
- 当所有的代码执行完后, 栈中只剩下window
var a = 10var bar = function (x) {var b = 5foo(x + b)}var foo = function (y) {var c = 5console.log(a + c + y)}bar(10)
4.面试题
/*测试题1: 先执行变量提升, 再执行函数提升*/function a() {}var aconsole.log(typeof a) // 'function'/*测试题2:*/if (!(b in window)) {var b = 1}console.log(b) // undefined/*测试题3:*/var c = 1function c(c) {console.log(c)var c = 3}c(2) // 报错
3.作用域与作用域链
1.作用域
- 理解
- 就是一块”地盘”, 一个代码段所在的区域
- 它是静态的(相对于上下文对象), 在编写代码时就确定了
- 分类
- 全局作用域
- 函数作用域
- 没有块作用域(ES6有了)
- 作用
- 隔离变量,不同作用域下同名变量不会有冲突
/* //没块作用域if(true) {var c = 3}console.log(c)*/var a = 10,b = 20function fn(x) {var a = 100,c = 300;console.log('fn()', a, b, c, x)function bar(x) {var a = 1000,d = 400console.log('bar()', a, b, c, d, x)}bar(100)bar(200)}fn(10)
2.作用域与执行上下文
- 区别1
- 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
- 函数执行上下文是在调用函数时, 函数体代码执行之前创建
- 区别2
- 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
- 执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放
- 联系
- 执行上下文(对象)是从属于所在的作用域
- 全局上下文环境==>全局作用域
- 函数上下文环境==>对应的函数使用域
var a = 10,b = 20function fn(x) {var a = 100,c = 300;console.log('fn()', a, b, c, x)function bar(x) {var a = 1000,d = 400console.log('bar()', a, b, c, d, x)}bar(100)bar(200)}fn(10)
3.作用域链
- 理解
- 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找的
- 查找一个变量的查找规则
- 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
- 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
- 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
var a = 1function fn1() {var b = 2function fn2() {var c = 3console.log(c)console.log(b)console.log(a)console.log(d)}fn2()}fn1()
4.闭包
1.理解闭包
- 如何产生闭包?
- 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包
- 闭包到底是什么?
- 使用chrome调试查看
- 理解一: 闭包是嵌套的内部函数(绝大部分人)
- 理解二: 包含被引用变量(函数)的对象(极少数人)
- 注意: 闭包存在于嵌套的内部函数中
- 产生闭包的条件?
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
function fn1 () {var a = 2var b = 'abc'function fn2 () { //执行函数定义就会产生闭包(不用调用内部函数)console.log(a)}// fn2()}fn1()function fun1() {var a = 3var fun2 = function () {console.log(a)}}fun1()
2.常见的闭包
- 将函数作为另一个函数的返回值
- 将函数作为实参传递给另一个函数调用
// 1. 将函数作为另一个函数的返回值function fn1() {var a = 2function fn2() {a++console.log(a)}return fn2}var f = fn1()f() // 3f() // 4// 2. 将函数作为实参传递给另一个函数调用function showDelay(msg, time) {setTimeout(function () {alert(msg)}, time)}showDelay('atguigu', 2000)
3.闭包的作用
- 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
- 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
问题:
1. 函数执行完后, 函数内部声明的局部变量是否还存在? 一般是不存在, 存在于闭中的变量才可能存在2. 在函数外部能直接访问函数内部的局部变量吗? 不能, 但我们可以通过闭包让外部操作它
function fn1() {var a = 2function fn2() {a++console.log(a)// return a}function fn3() {a--console.log(a)}return fn3}var f = fn1()f() // 1f() // 0
4.闭包的生命周期
- 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
- 死亡: 在嵌套的内部函数成为垃圾对象时
function fn1() {//此时闭包就已经产生了(函数提升, 内部函数对象已经创建了)var a = 2function fn2 () {a++console.log(a)}return fn2}var f = fn1()f() // 3f() // 4f = null //闭包死亡(包含闭包的函数对象成为垃圾对象)
5.闭包的应用
闭包的应用2 : 定义JS模块
- 具有特定功能的js文件
- 将所有的数据和功能都封装在一个函数内部(私有的)
- 只向外暴露一个包含n个方法的对象或函数
- 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
方式一:通过返回对象function myModule() {//私有数据var msg = 'My atguigu'//操作数据的函数function doSomething() {console.log('doSomething() '+msg.toUpperCase())}function doOtherthing () {console.log('doOtherthing() '+msg.toLowerCase())}//向外暴露对象(给外部使用的方法)return {doSomething: doSomething,doOtherthing: doOtherthing}}-------------------------------------------- HTML页面 ----------------------------------------------<script type="text/javascript" src="myModule.js"></script><script type="text/javascript">var module = myModule()module.doSomething()module.doOtherthing()</script>
方式二:通过立即执行函数,把属性赋值给全局变量(function () {//私有数据var msg = 'My atguigu'//操作数据的函数function doSomething() {console.log('doSomething() '+msg.toUpperCase())}function doOtherthing () {console.log('doOtherthing() '+msg.toLowerCase())}//向外暴露对象(给外部使用的方法)window.myModule2 = {doSomething: doSomething,doOtherthing: doOtherthing}})()------------------------------------------------- HTML页面 ----------------------------------------------<script type="text/javascript" src="myModule2.js"></script><script type="text/javascript">myModule2.doSomething()myModule2.doOtherthing()
6.闭包的缺点及解决
- 缺点
- 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
- 容易造成内存泄露
- 解决
- 能不用闭包就不用
- 及时释放
function fn1() {var arr = new Array[100000]function fn2() {console.log(arr.length)}return fn2}var f = fn1()f()f = null //让内部函数成为垃圾对象-->回收闭包
7.面试题
//代码片段一var name = "The Window";var object = {name : "My Object",getNameFunc : function(){return function(){return this.name;};}};alert(object.getNameFunc()()); //? the window//代码片段二var name2 = "The Window";var object2 = {name2 : "My Object",getNameFunc : function(){var that = this;return function(){return that.name2;};}};alert(object2.getNameFunc()()); //? my object
//终极闭包面试题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)//undefined,0,0,0var b = fun(0).fun(1).fun(2).fun(3)//undefined,0,1,2var c = fun(0).fun(1)c.fun(2)c.fun(3)//undefined,0,1,1
