2、函数高级
2.1、原型与原型链
2.1.1、prototype
2.1.1.1、 函数的prototype属性
每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象或者显示原型)
原型对象prototype中有一个属性constructor, 它指向函数对象
// 每个函数都有一个prototype属性,
//它默认指向一个对象(即称为: 原型对象)
console.log(Date.prototype, typeof Date.prototype);
function fn() {}
console.log(fn.prototype, typeof fn.prototype);
// 原型对象中有一个属性constructor, 它指向函数对象
console.log(Date.prototype.constructor === Date);
console.log(fn.prototype.constructor === fn);
2.1.1.2、给原型对象添加属性(一般都是方法)
作用: 函数的所有实例对象自动拥有原型中的属性(方法)
function F() {}
F.prototype.age = 12; //添加属性
F.prototype.setAge = function (age) {
// 添加方法
this.age = age;
};
// 创建函数的实例对象
var f = new F();
console.log(f.age);//12
f.setAge(23);
console.log(f.age);//23
2.1.2、显式原型与隐式原型
(1). 每个函数function都有一个prototype,即显式原型,是给程序员使用的
(2). 每个实例对象都有一个__proto__,可称为隐式原型,是js本身用的,一般情况,程序员不去使用
(3). 对象的隐式原型的值为其对应构造函数的显式原型的值
(4). 内存结构(图)
<script type="text/javascript">
function Fn() {
}
var fn = new Fn()
console.log(Fn.prototype, fn.__proto__)
console.log(Fn.prototype===fn.__proto__)//true
Fn.prototype.test = function () {
console.log('test()')
}
fn.test()
</script>
(5). 总结:
- 函数的prototype属性: 在定义函数时自动添加的, 默认值是一个空Object对象
- 对象的__proto__属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值
- __proto__和constructor属性是对象所独有的;
- prototype属性是函数所独有的,因为函数也是一种对象,所以函数也拥有__proto__和constructor属性。
- prototype属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即f1.__proto__ ===Foo.prototype。
- constructor属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向Function。
- 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)
2.1.3、原型链
2.1.3.1、原型链,别名: 隐式原型链(图解)
__proto__属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(父对象)里找,一直找,直到__proto__属性的终点null,再往上找就相当于在null上取值,会报错。通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链。
作用: 查找对象的属性(方法)
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.test3()//报错
2.1.3.2、构造函数/原型/实体对象的关系(图解)
2.1.4、属性问题
(1). 读取对象的属性值时: 会自动到原型链中查找
(2). 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
(3). 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.setName = function (name) {
this.name = name;
}
Person.prototype.sex = '男';
var p1 = new Person('Tom', 12)
p1.setName('Jack')
console.log(p1.name, p1.age, p1.sex,111)
p1.sex = '女'
console.log(p1.name, p1.age, p1.sex,222)
2.1.5、探索instanceof
1. instanceof是如何判断的?
表达式: A instanceof B
如果B函数的显式原型对象在A对象的原型链上, 返回true, 否则返回false
2. Function是通过new自己产生的实例
//案例1
function Foo() { }
var f1 = new Foo();
console.log(f1 instanceof Foo);//true
console.log(f1 instanceof Object);//true
//案例2
console.log(Object instanceof Function)//true
console.log(Object instanceof Object)//true
console.log(Function instanceof Object)//true
console.log(Function instanceof Function)//true
function Foo() {}
console.log(Object instanceof Foo);//false
console.log(Foo instanceof Object);//true
2.1.6、面试题
var A = function () {};
A.prototype.n = 1;
var b = new A(); //b.n=1
A.prototype = {
n: 2,
m: 3,
};
var c = new A();
// console.log(b.__proto__,c.__proto__);//{n:1},{n:2,m:3}
console.log(A.prototype); //{n:2,m:3}
console.log(b.n, b.m, c.n, c.m);
// 1/undefined/2/3
什么是原型,什么是原型链
2.2、闭包函数
2.2.1、闭包引入
<body>
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<!--需求: 点击某个按钮, 提示"点击的是第n个按钮"-->
<script type="text/javascript">
var btns = document.getElementsByTagName("button");
//有问题
// for (var i = 0, length = btns.length; i < length; i++) {
// btns[i].onclick = function () {
// alert("第" + (i + 1) + "个");
// };
// }
//解决一: 保存下标
/*for(var i=0,length=btns.length;i<length;i++) {
var btn = btns[i]
btn.index = i
btn.onclick = function () {
alert('第'+(this.index+1)+'个')
}
}*/
//解决办法: 利用闭包
for (var i = 0, length = btns.length; i < length; i++) {
(function (i) {
//行参i
var btn = btns[i];
btn.onclick = function () {
alert("第" + (i + 1) + "个");
};
})(i); //实参i
}
</script>
</body>
2.2.2、理解闭包
2.2.2.1、 如何产生闭包?
* 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 外部函数调用,就产生了闭包
2.2.2.2、闭包到底是什么?
* 使用chrome调试查看
* 理解一: 闭包是嵌套的内部函数(绝大部分人)
* 理解二: 包含被引用变量(函数)的对象(极少数人)
* 注意: 闭包存在于嵌套的内部函数中
function fn1 () {
var a = 3
function fn2 () {
console.log(a)
}
fn2()
}
fn1()
2.2.2.3、产生闭包的条件?
函数嵌套
内部函数引用了外部函数的数据(变量/函数)
调用了外部的函数
2.2.3、常见的闭包
2.2.3.1、将函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
当f()第二次执行的时候,a加1了,也就说明了:闭包里的数据没有消失,而是保存在了内存中。
如果没有闭包,代码执行完倒数第三行后,变量a就消失了。
上面的代码中,虽然调用了内部函数两次,但是,闭包对象只创建了一个。
也就是说,要看闭包对象创建了一个,就看:外部函数执行了几次(与内部函数执行几次无关)。
2.2.3.2、将函数作为实参传递给另一个函数调用
function showMsgDelay(msg, time) {
setTimeout(function () {
console.log(msg)
}, time)
}
showMsgDelay('hello', 1000)
//上面的代码中,闭包是里面的function,因为它是嵌套的子函数,而且引用了外部函数的变量msg。
2.2.4、闭包的作用
(1). 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
(2). 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
问题:
1. 函数执行完后, 函数内部声明的局部变量是否还存在?
一般是不存在的,存在闭包中的变量才可能存在
2. 在函数外部能直接访问函数内部的局部变量吗?
不能,但我们可以通过闭包让外部操作它
<script type="text/javascript">
function fun1() {
var a = 3;
function fun2() {
a++; //引用外部函数的变量--->产生闭包
console.log(a);
}
return fun2;
}
var f = fun1(); //由于f引用着内部的函数-->内部函数以及闭包都没有成为垃圾对象
f(); //间接操作了函数内部的局部变量
f();
</script>
2.2.5、闭包的生命周期
1. 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
2. 死亡: 在嵌套的内部函数成为垃圾对象时
<script type="text/javascript">
function fun1() {
//此处闭包已经产生
var a = 3;
function fun2() {
a++;
console.log(a);
}
return fun2;
}
var f = fun1();
f();
f();
f = null; //此时闭包对象死亡
</script>
2.2.6、闭包的应用-自定义js模块
* 创建具有特定功能的js文件
* 将所有的数据和功能都封装在一个函数内部(私有的)
* 只向外暴露一个包含n个方法的对象或函数
* 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
/**
* 自定义模块1
*/
function coolModule() {
//私有的数据
var msg = 'bdqn'
var names = ['I', 'Love', 'you']
//私有的操作数据的函数
function doSomething() {
console.log(msg.toUpperCase())
}
function doOtherthing() {
console.log(names.join(' '))
}
//向外暴露包含多个方法的对象
return {
doSomething: doSomething,
doOtherthing: doOtherthing
}
}
<script type="text/javascript" src="05_coolModule.js"></script>
<script type="text/javascript">
var module = coolModule()
module.doSomething()
module.doOtherthing()
</script>
/**
* 自定义模块2
*/
(function (window) {
//私有的数据
var msg = 'bdqn'
var names = ['I', 'Love', 'you']
//操作数据的函数
function a() {
console.log(msg.toUpperCase())
}
function b() {
console.log(names.join(' '))
}
window.coolModule2 = {
doSomething: a,
doOtherthing: b
}
})(window)
<script type="text/javascript" src="05_coolModule2.js"></script>
<script type="text/javascript">
coolModule2.doSomething()
coolModule2.doOtherthing()
</script>
2.2.7、闭包的缺点
(1). 缺点
* 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
* 容易造成内存泄露
(2). 解决
* 能不用闭包就不用
* 及时释放
<script type="text/javascript">
function fn1() {
var a = 2;
function fn2() {
a++;
console.log(a);
}
return fn2;
}
var f = fn1();
f(); // 3
f(); // 4
f = null // 释放
</script>
2.2.8、闭包面试题
<script type="text/javascript">
/*
说说它们的输出情况
*/
//代码片段一
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
return function () {
return this.name;
};
},
};
console.log(object.getNameFunc()()); //The Window
//代码片段二
var name2 = "The Window";
var object2 = {
name2: "My Object",
getNameFunc: function () {
var that = this;
return function () {
return that.name2;
};
},
};
console.log(object2.getNameFunc()()); //My Object
</script>
面试题:什么是闭包,有哪些缺点
一个函数(子函数)访问另一个函数(父函数)中的变量,外部函数调用,此时就有闭包产生,那么这个变量所在的函数我们就称之为闭包函数。
优缺点:优点:延伸了变量的生命周期,可以让外部操作函数内部的变量
缺点:变量生命周期长,可能会产生内存泄漏
如何解决:用完之后手动释放,或者尽量少用闭包
2.2.9、内存溢出与内存泄露
2.2.9.1. 内存溢出
* 一种程序运行出现的错误
* 当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误
2.2.9.2. 内存泄露
- 占用的内存没有及时释放
- 内存泄露积累多了就容易导致内存溢出
- 常见的内存泄露:
-
- 意外的全局变量
- 没有及时清理的事件回调函数
- 闭包
- 没有及时清理的定时器 setTimeout,setInterval
- 大量循环打印控制台日志
<script type="text/javascript">
// 1. 内存溢出
/*var obj = {}
for (var i = 0; i < 100000; i++) {
obj[i] = new Array(10000000)
}
console.log('------')*/
// 2. 内存泄露
// 意外的全局变量,变成了全局变量
function fn() {
a = []; //不小心没有var定义
}
fn();
// 没有及时清理的计时器
var iId = setInterval(function () {
console.log("----");
}, 1000);
clearInterval(iId);
</script>
2.3、递归函数
2.3.1、什么是递归函数?
递归是函数的高级用法,本质上是函数自已调用自已,它的行为非常类似循环
2.3.2、递归函数的特性
(1). 重复执行
(2).调用自身
(3). 【必须】要有条件控制,避免死循环,如果递归函数没有条件控制,那么他就是死循环
递归本身是一种循环操作,简单情况下可以替换循环语句的使用
注意:递归慎用,能用循环解决的事情,尽量别用递归
<script>
// 递归函数 :在函数内部调用自己,通过条件控制避免死循环
// 一直造成foo函数重复调用-- 死循环
var i = 0;
function foo() {
if (i >= 3) return;//限制条件
i++;
console.log("递归函数");
foo(); // 2.内部调用自己
}
foo(); // 1.外部调用
// 递归三特性-- 重复执行 / 调用自身 / 条件控制避免死循环!
</script>
2.3.3、递归函数常用案例
2.3.3.1、利用递归函数求1-100的和
<script>
// 利用递归函数求1-100的和
let i = 0
let sum = 0
function getSum() {
if (i >= 100) { return sum } // 后边只有一句话,可以省略大括号
i++
sum += i
getSum()
}
getSum()
console.log(sum);
</script>
2.3.3.2、斐波拉契数列
<!-- 经典案例2:斐波拉契数列
1,1,2,3,5,8,13,21,34,55,89...求第n项 -->
<script>
//递归方法
function fib(n) {
if (n === 1 || n === 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(10)); //34
//非递归方法
function fib(n) {
var a = 0;
var b = 1;
var c = a + b;
for (var i = 3; i < n; i++) {
a = b;
b = c;
c = a + b;
}
return c;
}
console.log(fib(10)); //34
</script>
2.3.3.3、爬楼梯
<!-- 经典案例3:爬楼梯
JS递归假如楼梯有 n 个台阶,每次可以走 1 个或 2 个台阶,
请问走完这 n 个台阶有几种走法 -->
<script>
function fun(n) {
if (n == 1) return 1;
if (n == 2) return 2;
return fun(n - 1) + fun(n - 2);
}
</script>