一.arguments
1.含义与作用
含义:arguments.callee代表当前的函数对象本身;
作用:在递归结构中,将被调函数的函数名换成arguments.callee,则该被调函数始终代表主调函数本身,与原递归结构相比,优化之处在于主调函数可随意修改函数名,函数体内部不需要随之修改,也不会受到影响
2.代码解析
1)arguments.callee:代表当前的函数对象本身
let fun = function(){
console.log(arguments.callee);
}
fun();---------------此时调用函数,则打印出函数fun本身,因为arguments.callee代表当前函数对象本身
2)arguments.callee的作用在递归思想中得以体现
function age(n){
let c;
if(n == 1){
c = 10;
}else{
c = age(n-1)+2;
}
return c;
}
console.log(age(5));
↓↓↓↓↓↓↓↓
function age(n){
let c;
if(n == 1){
c = 10;
}else{
c = arguments.callee(n-1)+2;--------------------将被调函数的函数名换成arguments.callee,则该被调函数始终代表主调函数本身,与原递归结构相比,优化之处在于主调函数可随意修改函数名,函数体内部不需要随之修改,也不会受到影响
}
return c;
}
console.log(age(5));
二.原型对象prototype
1.原型对象的概念,要点以及经典案例
概念:原型对象prototype,原型对象是函数对象(构造函数)的一个属性,它是用来保存所有实例对象共享的属性和方法的(也可以说是该类族所有的方法)
(对于类而言,prototype保存着所有实例的方法,平时我们在使用实例方法时,虽然用对象直接调用,但是真正的保存是在prototype中。我们创建的每个函数都有一个属性是prototype(原型),该对象的用途是包含所有实例共享的属性和方法。所有通过同一个构造函数创建的实例对象,都会共享同一个prototype。)
要点:实例化对象不可以改变原型对象的属性或方法;如果实例化对象修改该属性,效果等价于为自身添加了一个新的同名属性(触发暂时性死区,自身同名属性屏蔽原型对象的公共属性,但原型对象则不会受到影响发生改变),但是如果使用实例化对象的__proto__属性去访问原型对象并修改,则可以修改原型对象prototype中的属性与方法
注意点(重点):为什么实例化对象可以访问所有的属性和方法?
1)实例化对象可以直接访问自己开辟空间的属性,
2)每个实例化对象都有一个__proto__的属性,该属性(保存原型对象prototype的地址)指向类的原型对象prototype,所以实例化对象可以访问原型对象上的属性或者方法(实例对象.__proto__ == 类.prototype)(__proto__该属性前后都是双下划线)
2.代码解析
ES5函数模拟类(创建类)
function Student(id,name){
this.id = id;
this.name = name;
this.study = function(){
console.log("study");
}
this.eat = function(){
console.log("eat");
}
}
创建实例化对象:
let a = new Student(12,'大白');
let b = new Student(15,'小黑');
在创建以上两个实例化对象时,我们发现,两个对象共开辟两份堆空间内存,其中属性
this.id与this.name各不相同,因此需要两份空间分别存储,但两个实例化对象中的方法
this.study与this.eat却是完全相同的,完全相同的方法却占用了两份空间,这显然是不合理
的;
由此衍生出核心问题:行为方法不应该属于每一个实例化对象,它们应该属于整个类族,且只有一份;
要解决这个问题,我们就需要用到原型对象prototype
function Student(id,name){
this.id = id;
this.name = name;
this.study = function(){ --------------------------在原本类的基础上,去掉这两个方法,在类的外部使用原型对象重新创建行为方法
console.log("study");
}
this.eat = function(){
console.log("eat");
}
}
Student.prototype.study = function(){----------------------此时这两个行为方法只占用一份堆空间,且以Student类为模板创建的实例化对象都可以调用
console.log(this.name+" study");
}
Student.prototype.eat = function(){
console.log(this.name+"eat");
}
Student.prototype.teacher = "大黄";-------------------原型对象也可以创建实例化对象的公用属性
Student.prototype.teacher = "小黄";-------------------相同名称的公用属性后者会覆盖前者
let s1 = new Student(1,"凢凢");--------------此时创建的两个实例化对象所占据的堆空间不包含公用属性和行为方法的空间
let s2 = new Student(2,"王一博");
s1.study();
s1.eat();
s2.study();
s2.eat();
s1.teacher = "曹柏林";----------------实例化对象不能通过修改自身属性去改变公用属性,这里的s1只是为自身添加了一个teacher属性,属性值为"曹柏林",但原型对象中teacher属性的属性值没有被改变,其他实例化对象调用时,属性值还是"小黄"(此处相当于触发暂时性死区,s1的teacher作为内部变量屏蔽类的原型对象中的teacher);
delete s1.teacher;----------------------------取消实例化对象为自己添加的新属性写法,该条语句之后,实例化对象再次调用该属性,则是在使用原型对象中的该属性(属性值回复成为‘小黄’)
3.案例
需求:我们可以试着为系统提供固定属性与方法的容器Array数组添加新的API(方法);
Array.prototype.max = function(){------------------数组Array原本是没有max这个API的,但我们可以利用原型对象创建一个暂时的公用API,也就是数组的新API。此处我们可以利用循环结构,使max方法中找到数组数值最大的元素并返回,于是这里的max就变成了数组的专属Math.max();
let x = this[0];
for(let i=0; i<this.length; i++){
if(x < this[i]){
x = this[i];
}
}
return x;
}
let arr = [1,2,13,4,15];-----------------创建数组的实例化对象,就可以调用原型对象创建的公用方法
console.log(arr.max());
三.apply和call
1.他们的使用方法以及注意点
作用:apply和call都是用来改变具名函数this指向的函数(类和函数的解耦)
1)函数对象.apply(被修改的this指向,[函数对象参数1,参数2...]);
2)函数对象.call(被修改的this指向,函数对象参数1,函数对象参数2...);
重要点:apply,call,bind的异同?
1)都是用来修改this指向的,bind通常用来修改匿名函数;apply,call修改有名函数
2)apply,call第二个参数不同,apply的第二个参数必须是一个数组;bind的参数和call一样
3)apply,call是直接调用该函数,bind是产生了一个待调用的新的函数对象
2.代码解析
function Monkey(name){
this.name = name;
}
Monkey.prototype.eat = function(food1,food2){
console.log(this.name + " " + food1 + " " + food2);
}
function Snake(name){
this.name = name;
}
Snake.prototype.eat = function(food1,food2){
console.log(this.name + " " + food1 + " " + food2);
}
let m = new Monkey("熏悟空");
m.eat("桃子","香蕉");
let s = new Snake("白素贞");
s.eat("许仙","法海");
需求:不同的类(Monkey和Snake)拥有类似的this.eat方法,能否复用this.eat方法?
(从两个不同类的原型对象创建的行为方法变成一个可以被不同的类公用的行为方法);
方案:可以将原型对象创建的方法改成普通函数创建的方法,然后在调用时改变this指向(这里指的是普通函数的外部调用,内部调用this的指向本就是调用对象本身)
function Monkey(name){
this.name = name;
}
function Snake(name){
this.name = name;
}
function eat(food1,food2){-----------------------将原本两个不同的原型对象创建方法改写成预备公用的普通函数方法
console.log(this.name + " " + food1 + " " + food2);
}
let m = new Monkey("熏悟空");--------------创建类的实例化对象
eat.call(m,"桃子","葡萄");----------------在调用普通函数的方法时,使用apply或者call改变普通函数中的this指向,使普通函数中的this指向调用它的实例化对象;
eat.apply(m,["香蕉","人参果"]);
let s = new Snake("大蛇丸");
eat.call(s,"老鼠","青蛙");
eat.apply(s,["兔子","猴子"]);
eat.bind(m,"桃子","葡萄");------------此处直接使用bind改变this指向并不能达到apply与call的效果,因为bind的作用等价于创建了一个(待调用)的新的函数对象,他不会直接调用函数。
正确写法:
let f = eat.bind(m,"桃子","葡萄");-------------使用变量容器接收新的函数对象,然后调用即可。
f();
四.柯里化函数
1.作用以及他们的概念
概念:柯里化函数是一个包含一个参数并且返回一个函数的函数
arity(参数个数)是函数所需的形参的数量。
函数柯里化(Currying)意思是把接受多个 arity 的函数变换成接受单一 arity 的函数。
换句话说,就是重构函数让它接收一个参数,然后返回接收下一个参数的函数,依此类推。
作用:
1.柯里化把简单的问题复杂化,但是复杂化的同时,使函数使用有更多的自由度。
2.柯里化的核心:对于函数参数的自由处理。 本质上是降低通用性(适用范围),提高适
用性。
2.代码解析
function add(x,y){
return x + y;
}
console.log(add(1,5));
改写柯里化函数↓↓↓↓
function add(x){
return function(y){
return x + y;
}
}
console.log(add(5)(6));
案例:
function checkReg(reg,str){
return reg.test(str);
}
判断用户名
console.log(checkReg(/^\w{6,18}$/,"heihei"));
console.log(checkReg(/^\w{6,18}$/,"123"));
console.log(checkReg(/^\w{6,18}$/,"laowang"));
判断密码
console.log(checkReg(/^.{6,}$/,"123"));
console.log(checkReg(/^.{6,}$/,"11111111"));
console.log(checkReg(/^.{6,}$/,"123"));
上述写法缺点:
校验同一类型的数据时,相同的正则写了很多次
代码可读性较差,如果没有注释,我们并不能一下就看出来正则的作用
改写为柯里化函数↓↓↓↓
function checkReg(reg){
return function(str){
return reg.test(str);
}
}
生成工具函数f1,判断用户名
let f1 = checkReg(/^\w{6,18}$/);
console.log(f1("heihei"));
console.log(f1("1231"));
console.log(f1("laowang"));
生成工具函数f2,判断密码
let f2 = checkReg(/^.{6,}$/);
console.log(f2("123"));
console.log(f2("helloworld"));