跳至主要內容

10. 函数对象

Harry Xiong大约 32 分钟Web 前端JavaScriptPromise 对象JS 异步async 与 await

函数对象

函数也是也是对象,函数中可以封装一些功能(代码),在需要的时候执行,并且函数对象具有所有普通对象所具备的

功能

//创建和使用函数
function test(){
    console.log(1);
}
//封装到函数中的代码不会立即执行,函数中的代码会在函数调用的时候执行

//调用函数
test();

console.log(typeof test);
//使用typeof检查一个函数对象时,会返回function,因为函数在js中占了很大的比重,所以把函数单独拿出来当做一个种类,实际上还是一个对象

10.1 创建函数对象

  • 构造函数方法创建函数对象,将要封装的代码以字符串的形式传递给构造函数(不推荐使用这种方法)

    var fun=new Function("console.log('HELLO');");
    //这种构成函数的方法Function里面必须是用""括起来的字符串
    
  • 函数声明方法创建函数对象

    /*
    语法:
    function 函数名([形参1,形参2]){
        功能片段
    }
    形参的[]可以省略,在()里写上对应的变量,不用写var
    */
    function fun(){
        console.log("HELLO");
    }
    
  • 3.函数表达式创建函数对象

    /*
    语法:
    var 函数名=function([形参]){
        功能片段
    }
    */
    Var fun3=function(){
        console.log("HELLO");
    };
    fun3();
    //上方相当于把一个函数赋值给了fun3
    

注意:

  • 加括号是调用函数,相当于函数的返回值,不加括号是函数对象,相当于直接使用函数对象.把函数赋值给一 个变量时,只需要将函数名赋值过去,不要加后面的(),如果是没有返回值的函数就会把undefined赋值给变量,如 果有返回值就把返回值赋值给它,同时会运行函数

  • 调用函数时解析器不会检查实参的类型,注意是否有可能会接收到非法的参数,如果有可能则需要对参数类型 进行检查

  • 调用函数时也不会检查实参的数量,多余的实参不会被赋值,如果实参的数量少于形参的数量,则没有对应的 形参将会是undefined

10.2 函数的返回值

return返回一个返回值,在函数return后面的函数都不会执行,直接结束该函数

return后面可以跟任意数据类型,可以是一个对象,也可以是一个函数,如果return语句后不跟任何值就相当于返回

undefined,如果不写return也会返回undefined

  • 返回值为普通数据类型时
//如:定义一个函数,判断一个数字是否是偶数,如果是返回true,否则返回false
function isou(nume){
return num%2==0;
}
console.log("result="+isou(15));
  • 返回值为函数时
//在一个函数内部可以无限次的嵌套函数,同时可以嵌套调用函数
function fun1(){
    
function fun2(){
console.log("123");
}
    
fun2();
    
}
fun1();
//注意:因为fun2是在fun1里面创建的,所以只能在fun1里面用


function fun3(){

function fun4(){
    alert("hello");
}

return fun4;

}
a=fun3();
console.log(a)//这时a是一个函数对象,可以通过a();fun4函数
              //同时还可以fun3()(); 这种写法和上面一样,只不过没有又创建一个对象

10.3 函数的参数

函数的参数分为实参和形参,形参是创建函数的时候写入,形参则是在调用函数的时候写入

//如果参数过多容易被弄混乱,可以将参数封装到一个对象中,在里面就可以用对象的值,只要属性名正确即可
function fun(obj){
    alert(obj.name)
}
var obj={
name:"孙悟空",
age:18
}

fun(obj);
//实参可以是一个对象,也可以是一个函数 
function fun2(a){
    a(obj);
}
fun2(fun);
//在这个函数中a就是一整个fun函数,也就是函数赋值,把地址赋值给a

//另一种写法:直接将函数传入
fun2(function(){alert("hello")});

10.3.1 有初始值的形参

在定义形参的时候为形参设置初始值(ES6新增语法),在没有传入实参的情况下形参会默认地使用初始值

function fun(a=123){//因为是ES6的语法,所以这里的a是通过let方法声明的,与后边的作用域有很大联系
    console.log(a); 
}
fun();//打印出123

10.3.2 封装实参的对象arguments

在调用函数时,浏览器每次会传递两个隐含的参数(箭头函数没有这两个参数)

  • 函数上下文对象this

  • 封装实参的对象arguments

arguments是一个类数组对象,很数组很像但不是数组,它可以通过索引来操作数据,也可以用length获取长

度,在调用函数时,我们所传递的实参都会在arguments中按照传入的顺序保存

function fun(){
    console.log(arguments.length)
}
//可以用console.log(arguments.length)来查看函数的实参个数,如果没有实参长度就是0

**注意:**即使不在创建函数的时候写形参,如果依然传入了实参,实参也会保留在arguments中,并且可以调用,

//argument中有一个属性callee,这个属性对应当前正在执行的函数对象
Function fun(){
    Console.log(arguments.callee===fun)//返回true
}

10.3.3 this

this解析器在调用函数时传递给函数的一个参数,this指向的是一个对象,这个对象我们称为函数指向的上下文对象,根据函数的调用方式不同(跟创建方式无关),this会指向不同的对象

this指向

  • 以函数形式调用,this永远是window(非严格模式,严格模式是undefined,ES6中的class构造函数就是开启严格

    模式)

    "use strict";//在最开头写上这段代码解析器自动开启严格模式解析
    function fun(){
        console.log(this);//undefined
    }
    fun();
    
  • 以方法的形式调用,this被调用的那个对象

  • 以构造函数的形式调用时,this就是创建的那个对象

  • 使用call和apply调用时,this就是指定的那个对象

10.3.4 rest参数

rest参数写在函数创建形参时的最后一位,用作收集剩余传入的实参,写法为...数组名

function show(a, b, ...args){
    console.log(a);
    console.log(b);
    console.log(args);
}

show(1, 2, 3, 4, 5);//此时args为一个有着3,4,5值的数组
//rest参数还能用作展开数组,效果和将数组直接拆分开一样,这个用法很有用
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let arr3 = [...arr1, ...arr2];//相当于直接将arr1和arr2拆分开
console.log(arr3);

10.3.5 其他属性参数

在函数创建时会默认传入很多的属性参数,这些参数通常情况下很少用

  • name属性,函数的名字,name属性是只读的,不能够修改

  • arguments属性,这个属性同上面的arguments属性是一样的,只是用作了函数的属性来调用

  • length属性,函数定义时形参的个数,但是不包括有默认值的形参和rest形参

  • caller属性,说明调用者的属性(在下方实例进行说明)

fun1.name="fun3";//因为name属性时只读的,这行代码没有作用
fun1(1,2,3,4);//fun1.name为"fun1",fun1.length为1,fun1.arguments.length为4
console.dir(fun1);//console.dir可以显示一个对象的所有属性和方法

fun2();//fun1.caller为"fun2"

function fun1(x=0,y){
    console.log(fun1.name);
    console.log(fun1.arguments.length);//和arguments.length作用相同
    console.log(fun1.length);
    console.log(fun1.caller);
}

function fun2(){
    fun1(1,2);//这里就是在fun2中调用fun1,fun1.caller就是"fun2"
}

10.4 立即执行函数

立即执行的函数都是通过函数表达式的方法声明的,所以叫作立即执行函数表达式(IIFE)

函数定义完成后,立即被调用,这种函数叫立即执行函数,立即执行函数往往只会执行一次

//第一种方式,函数用小括号包起来,然后后面加小括号
(function(a,b){
    console.log(a+b)
})(10,1000);
//必须要加一个()在前面将function(){}包起来.表示是一个整体,同时说明被括起来的部分是一个函数对象
//后面跟()立即调用它,同时也可以带入参数进去

//第二种方式,函数后面加用小括号,然后再用小括号包起来
(function(a,b){
    console.log(a+b)
}(10,1000));

//第三种方式,函数后面加小括号,然后在函数前面加+ - ~ !其中一个符号
+function(a,b){
    console.log(a+b)
}(10,1000)

-function(a,b){
    console.log(a+b)
}(10,1000)

~function(a,b){
    console.log(a+b)
}(10,1000)

!function(a,b){
    console.log(a+b)
}(10,1000)

**立即执行函数的用处:**将在一个script标签中写的代码通过一个立即执行函数包裹起来,可以避免污染全局环境

10.5 方法

对象的属性值可以是任何数据类型,也可以是一个函数,如果一个函数作为一个对象的属性保存,那么我们称这个

函数是这个对象的方法,调用用这个函数就是用这个对象的方法,但是这只是名称上的区别,并没有其他的区别

比如console.log()就是调用console对象的log方法

10.6 作用域

作用域指一个变量的作用的范围,在JS中一共有三种作用域(ES5为两种,ES6有了块作用域,需要用let或const申明)

10.6.1 全局作用域

直接编写在script标签的js代码,都在全局作用域中,全局作用域在页面打开时创建,在页面关闭时销毁

其他作用域可以访问到全局作用域中的变量.全局作用域不能访问其他作用域的变量

  • 在全局作用域中有一个全局对象window,它代表一个浏览器的窗口,由浏览器创建,我们可以直接使用
//在全局作用域中,创建的变量都会作为window对象属性保存
var a=10;
console.log(window.a);//输出的结果依旧为10

注意:window对象实际可以说是比全局作用域还要高一个层次,因为一个页面可以引入很多script标签,每个script标签中的全局环境是相互独立的,但是所有script标签都只有一个window

  • 一个变量没有声明就使用会报错,但是如果通过作为window对象的属性来调用就不会报错,这个值会是 undefined
console.log(a);//a没有被声明,会报错
console.log(window.a)//返回undefined
  • 无论什么作用域下,只要没有声明就使用的变量,会成为类似全局作用域的变量,因为没有声明就赋值的变量会泄

    露出来,成为全局对象window的属性

    <!--
        要特别注意全局变量泄露污染了全局变量window的环境
    -->
    <script>
        a=10;//a泄露给window成为window的属性
    </script>
    
    <script>
    console.log(a);//通过window访问到了另外一个全局作用域的a,这里不会报错
    </script>
    
  • 用var声明的全局变量也会成为window对象下的属性,而用ES6中的let和const则不会成为window的属性,但 是这三者全都可以被其他script标签访问到

  • 使用函数声明方法创建的函数都会作为window的方法保存

    function fun(){
        alert(1);
    }
    window.fun();//用这种方法也能调用
    
    window.alert(123);
    /*
    alert()也是调用函数,所以alert也是一个函数对象,可以window.alert(),也可以alert(),这是浏览器自己创建的函数  
    */
    

10.6.2 函数作用域

函数作用域是在function字样里的作用域,调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁

每调用一次函数就会创建一个新的函数作用域,它们之间互相独立的

注意:

  • 当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果没有,再在离得最近的一级作用域,直到 找到全局变量,如果全局作用域中没有找到,就会报错

  • 如果想直接在函数作用域中用全局的变量,使用window.a来调用(前提是用var或者直接写来声明的变量)

  • 在函数作用域中也有声明提前的特性,使用变量声明关键字声明的变量,会在函数中所有的代码执行之前被声 明,这个变量是在函数作用域中被声明的,同理在函数中声明的函数也是一样

  • 有参的函数在调用的时候不传入实参,形参的值是undefined,因为在定义形参的时候相当于在函数内部用var声 明了一个变量,没有赋值的变量一律为undefined,即使全局变量中有一样的变量也会用内部没有赋值的变量

10.6.3 块作用域

块作用域是在{}之内的作用域,块作用域的变量只能由ES6语法中的let和const创建

{
    let a=123;
}
console.log(a);//报错,只有var声明或者直接写的时候才能访问到

**注意:**块级作用域中的变量使用条件和函数作用域基本相同,但是在使用时的情况会有所不同

块作用域的用处体现

for(let i=0;i<10;i++){
    div.onclick=function(){
        console.log(i);
    }
}
//打印出的是0到9

for(var i=0;i<10;i++){
    div.onclick=function(){
        console.log(i);
    }
}
//打印出的是10个10

10.6.4 声明提前

10.6.4.1 变量的声明提前

使用var关键词声明的变量,会在所有的代码执行之前被声明,而let和const不会

/*只写a变量不写var,则a相当于window.a,但是两者区别在于用var声明的变量能够声明提前*/
console.log(a);//在这里会报错
a=123;

console.log(b);//这里不会报错而是会输出undefined
var b=123;

//相当于
var b;
console.log(b);
b=123
console.log(c);
let c=123;
/*
在c被用let声明赋值之前的一段区域叫作c的暂时性死区(TDZ)(这是ES6独有的),在这一个区域里面c是不允许被调用
的,否则就会报错
*/

//再如:
let a=123;
function(a=a){//报错
    console.log(a);
}
/*
在设置形参的默认值时,会在()内临时形参一个作用域,由于找一个变量的时候会先在自身作用域中寻找,由于js的机制
是先编译后执行的,所以先找到自身作用域中的a,但是a并没有被赋值就被使用,陷入了暂时性死区,所以会报错
*/
10.6.4.2 函数的声明提前

使用函数声明形式创建的函数会在所有代码执行之前就被创建(在var声明的变量之后执行,所以在编译期用var声

明的变量与函数变量的变量名矛盾的时候函数的变量会将var声明的变量覆盖),所以可以在函数声明前调用函数,

这个形式声明的函数会默认放在当前作用域代码最上面

fun();//能正常打印123

function fun(){
    console.log(123);
}

**使用函数的表达式创建的函数本身不会声明提前,**只会先将用var创建的变量提前

fun2();//报错

var fun2=function(){
    console.log(123);
}
/*
    使用函数表达式创建的函数只会先创建出变量,这个变量声明提前,但是此时的值是undefined,而undefined不    是一个函数,所以在调用的时候会报错
*/

10.7 箭头函数

箭头函数为ES6中函数的简写,省略了写function的形式,而改用=>来代替

// 普通函数
function name() {
}
// 箭头函数,去掉 function,加上 =>
() => {
}
let show1 = function () {
    console.log('abc');
}
let show2 = () => {
    console.log('abc');
}
show1();
show2();
let show4 = function (a) {
    return a*2;
}
/*
1.如果只有一个参数,()可以省
2.如果函数只有reutrn这一个句子,{}可以省
*/
let show5 = a => a * 2
console.log(show4(10))
console.log(show5(10))

注意:

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象

  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会报错

  • 不可以使用arguments对象,该对象在函数体内不存在,如果要用,用rest参数代替

  • 不可以使用yield命令,因此箭头函数不能用作Generator函数

10.8 工厂方法创建对象

使用工厂方法创建对象可以大批量的创建属性相同的对象

function create(name,age,gender){
Var obj=new Object();
obj.name=name;
obj.age=age;
obj.gender=gender
return obj;
}

var obj1=create(name,age,gender);
var obj2=create(name,age,gender);

**注意:**使用工厂方法创建的对象,使用的构造函数都是Object,所以创建的对象都是Object这个类型,导致我们无

法区分出多种不同类型的对象,所以这个方法很少用,经常使用构造函数的方法

10.9 构造函数

使用构造函数创建函数,创建的函数和普通函数没有本质区别,只是构造函数习惯上首字母大写

构造函数和普通函数的本质区别是调用方式不同,普通函数是直接调用,而构造函数需要使用new关键字来使用,一般通过构造函数来实现面向对象编程

面向对象

JS通过原型链构成的模拟了类,通过这个模拟类我们可以实现面向对象编程(在ES6中被推出了class关键字,但是依然是通过原型链模拟出的类),但是并不是其它面对对象编程语言中的真正的类,所以一般我们说JS是一门基于对象的变成语言

面向对象的三大特征

  • 封装,对象与对象之间相互独立,不会影响对方

  • 继承,能传递给子对象

  • 多态,传入不同的参数表现不同的效果

function Person(name){
    this.name=name
}       

  var per= new Person("孙悟空");
  console.log(per);//打印的是一个Person对象,如果不加new则per的值为undefined

**注意:**new的构造函数后面加括号与不加括号的有区别,但无本质区别,这里具体见http://www.cnblogs.com/xwant/p/7752658.htmlopen in new window

构造函数的执行流程

1.立刻创建一个新的对象

2.将新建的对象设置为函数中的this,在构造函数中可以使用this来引用新建的对象

3.执行函数中的代码

4.将新建的对象作为返回值返回

使用同一个构造函数创建的对象,我们称为一类对象,也将一个构造函数称为一个类,我们将通过一个构造函数的对象,称为是该类的实例

//可以使用instanceof可以检验一个对象是否是一个类的实例

function Person(name){
    this.name=name
}       
  var per= new Person("孙悟空");
  console.log(per instanceof Person);//如果per是Person类的实例返回true,否则返回false
  
  console.log(per instanceof Object);
//所有的对象都是Object的后代,所有任何对象和Object与instanceof检验都会返回true

在构造函数中定义方法

function Person(name,age,gender){
    this.name=name;
    this.age=age;
    this.gender=gender;
    this.sayName=function(){
        alert("hello");
    };
}
var per=new Person("孙悟空",18,"男");
//但是上面当调用方法的时候没调用一次就要重新创建一次方法,会影响性能

//使用下面的方法可以只用创建一次函数,节省了性能,但是将函数定义在全局作用域,污染了全局作用域的命名空间
this.sayNaem=fun;

funciton fun(){
alert("hello");
};

//我们一般要为对象添加方法时都是通过给原型添加方法从而做到给这个对象添加方法

10.10 函数原型对象prototype

只有函数对象才有原型这个属性,每次创建函数时,解析器都会向函数中添加一个属性prototype,这个属性对应一个

对象,这个对象就是我们所谓的原型对象

如果函数作为普通函数调用prototype没有任何作用,当函数以构造函数的形式调用时,它所创建的对象都会有一个隐含属性 proto ,指向改构造函数的原型对象prototype

**注意:prototype只有函数才有,而一个对象是没有这个属性的,但是两者都有__ proto __,**一个函数的prototype也

是通过构造函数创造的,用的是new Object()构造函数

function Person(name,age,gender){
    this.name=name;
    this.age=age;
    this.gender=gender;
}
var per=new Person("孙悟空",18,"男");
console.log(Person.prototype===per.__proto__);//返回true
//证明构造函数的prototype属性和per的__proto__属性的指向是相同的

原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问这个原型对象,我们可以将对象中共有的

内容,统一到原型对象中,但我们访问对象的一个属性或方法时,它会现在对象自身中寻找,如果有则直接使

用,如果没有则会去原型对象中寻找,如果找到则直接使用

function Person(name,age,gender){
    this.name=name;
    this.age=age;
    this.gender=gender;
}
//向Person的原型中添加属性a
Person.prototype.a=123;
//向Person的原型中添加方法sayName
Person.prototype.sayName=function(){
    alert("hello"+this.name);
}
var per=new Person("孙悟空",18,"男");
console.log(per.a);
per.sayName();

/*
以后创建构造函数时,可以将这些对象共有的属性和方法,统一添加到构造函数的原型对象中,这样不用分别为每一个对象添加,也不会影响到全局作用域,就可以使每一个对象都具有这些属性和方法
*/

注意:

  • 通过原型对象添加的属性和方法函数本身是没有的,这就是静态方法和实例方法的区别

  • 在前面我们知道可以用 "属性名" in 对象 来检验对象中是否含有某个属性,但是如果对象自身没有该属性中但是

    原型对象中有该属性,也会返回true,所以为了区分是否是自己本身的属性,可以使用

    hasOwnProperty()

    方法来检查对象自身中是否含有该属性

    function Person(name,age,gender){
        this.name=name;
        this.age=age;
        this.gender=gender;
    }
    Person.prototype.a=123;
    var per=new Person("孙悟空",18,"男");
    console.log("a" in per);//true
    console.log(per.hasOwnProperty("a"));//false
    

    注意:

    测试的对象自身并没有hasOwnProperty方法,并且该方法也不在它的原型中,而是在Object的原型中

    function Person(name,age,gender){
        this.name=name;
        this.age=age;
        this.gender=gender;
    }
    Person.prototype.a=123;
    var per=new Person("孙悟空",18,"男");
    console.log(per.hasOwnProperty("per.hasOwnPropert"));//false
    

    原因:

    原型对象也是一个对象,说明也有一个原型

    proto

    ,当我们使用一个对象的属性或者方法的时候,会先在

    自身中寻找,如果有则直接使用,如果没有则去原型对象中寻找,如果原型对象中有,则使用,如果没有则去

    原型的原型中寻找,直到找到Object对象的原型,Object对象的原型没有原型,如果在Object中依然没有找

    到,则返回undefined

10.11 继承

10.11.1 组合继承

原型可以只通过构造函数再次获得继承,能够继承父级别的方法,但是这样有缺陷,每一次在设置属性的时候都需要再次设置相同的属性,而且如果通过父类的原型设置属性容易造成值重复的情况(这种方式还会造成数据共享)

function Person(name, age, gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
}
Person.prototype.sayHello=function(){
    alert("hello");
}
Person.prototype.name="孙悟空";
/*
下面这种写法更加简便
Person.prototype={
    sayHello:function(){
        alert("hello");
    },
    name="孙悟空"
}
*/
function Student(name,age,gender,grade){//在这里因为是设置了name属性,如果没有设置所有通过
        this.name = name;               //Student类创建的对象的name都是孙悟空
        this.age = age;
        this.gender = gender;
        this.grade.grade
}
Student.prototype=new Person();//虽然这样Student类能继承Person类的sayHello方法,但是需要再设置                               //属性

解决方案:

  • 继承属性使用父类.call(参数)

  • 通过函数的原型相同实现继承方法

function Person(name, age, gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
}
Person.prototype.sayHello=function(){
    alert("hello");
}
function Student(name,age,gender,grade){
        Person.call(this,name,age,gender);//解决了属性继承,并且值不重复的问题
        this.grade=grade;
}
Student.prototype=Person.prototype;
//虽然能继承到父类的方法,但是却将两者的内存统一了,如果修改子类的prototype父类的protptype也会变化

解决方案:

  • 继承属性使用父类.call(参数)

  • 对原型使用构造函数

function Person(name, age, gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
}
Person.prototype.sayHello=function(){
    alert("hello");
}
function Student(name,age,gender,grade){
        Person.call(this,name,age,gender);
        this.grade=grade;
}
Student.prototype=new Person();
/*
    虽然这样基本不会影响效果,但是在观察一个由子类创建的对象可以发现虽然子类上继承的属性有正确的属性值,
    但是其父类上依旧显示有该属性,并且该属性的属性值为undefined,会造成属性结构的杂乱
*/

最终解决方案:

  • 继承属性使用父类.call(参数)

  • 借助一个无用的构造函数

  • 对原型使用无用的构造函数

function Person(name, age, gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
}
Person.prototype.sayHello=function(){
    alert("hello");
}
function Student(name,age,gender,grade){
        Person.call(this,name,age,gender);
        this.grade=grade;
}
function Exchange(){
    
}

Exchange.prototype=Person.prototype;//用无用的构造函数的prototype来指向Person的prototype
Student.prototype=new Exchange();

也可以用Object.create()方法来继承原型,同时也不会造成结构杂乱

function Person(name, age, gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
}
Person.prototype.sayHello=function(){
    alert("hello");
}
function Student(name,age,gender,grade){
        Person.call(this,name,age,gender);
        this.grade=grade;
}
Student.prototype = Object.create(Person.prototype);

注意:在继承的时候需要重新设置一下constructor,使用A.prototype.constructor=A来设置

为什么需要做A.prototype.constructor=A这样的修正?

  • 在构造函数时new 函数名() 本质上是通过函数原型中的constructor()构造函数方法(该方法是创建函数时就有的属性),所以new 函数名()和new 函数名.prototype.construstor()是一样的效果

    function Person(name, age, gender) {
            this.name = name;
            this.age = age;
            this.gender = gender;
    }
    var per = new Person.prototype.constructor("孙悟空", 18, "男");
    console.log(per);
    
  • 在原型继承的时候,我们将上级函数的实例赋值给了下级函数的原型,所以就会造成下级函数原有的原型发生彻

    底的改变。因为constructor属性是原型默认的两个属性之一。该属性在正常情况下是等于其对应的函数本身

    的。但是在实现原型继承之后,下级函数的constructor属性就会发生改变,变为了等于上级函数本身。但是如果

    我们不进行修正的话也可以正常使用,不会发生什么问题。那么我们修正这个constructor属性就只是为了防止

    一种情况下出错:在不知道某个对象是哪个函数实例化的时候想要克隆一个对象

    //例如,我不知道person对象是在哪个函数实例化出来的,但是我想克隆一个,这时候就可以这样
    function Person(name, age, gender) {
            this.name = name;
            this.age = age;
            this.gender = gender;
    }
    
    function Student(name,age,gender,grade){
        Person.call(this,name,age,gender);
        this.grade=grade;
    }
    function Exchange(){
        
    }
    
    Exchange.prototype=Person.prototype;
    Student.prototype=new Exchange();
    
    Student.prototype.constructor=Student;
    var stu = new Student("孙悟空", 18, "男",100);
    var stu1 = new stu.constructor();//而如果在继承时没有通过重新设置construct,那么克隆后的对象
                                     //就相当于是通过new Person()创建的,没有grade属性
    
    //可以写一个继承方法
    function extend(child,parent){
    child.prototype=new parent();
    child.prototype.constructor=child;
    }
    

10.11.2 拷贝继承

通过拷贝将函数原型中的属性和方法复制,达到继承的目的

function Person(name, age, gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
}
Person.prototype.sayHello=function(){
    alert("hello");
}
Person.prototype.a=123;

function Student(name,age,gender,grade){
    Person.call(this,name,age,gender);
    this.grade=grade;
}
//子类拷贝父类原型继承父类原型方法
for(var key in Person.prototype){
    Student.prototype[key]=Person.prototype[key];
}


var obj={}; 
for(var key in Person.prototype){
    obj[key]=Person.prototype[key];
}
console.log(obj);
console.log(obj.a);//123
obj.sayHello();

上面的方式是浅拷贝,虽然能够获得相同的值,该对象中对象或数组的值的地址并没有发生改变,还是被拷贝者的地址

在ES6中Object.assign()的效果和浅拷贝的效果相同,但该函数可以继承多个类

Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象并返回目标对象,可以放入多

个参数,第一个是要被放入的对象,可以直接一个空对象,后面的参数是要拷贝的对象,可以传入多个对象把所有对象

的属性或者方法都传入到前面的空对象里,最后将结果返回出来

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}
// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合多个类的继承类
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     
};
const object1 = {
  a: 1,
  b: 2,
  c: 3
};
const object2 = Object.assign({c: 4, d: 5}, object1);//实际上这种叫做根元素深复制,其他元素浅
console.log(object2.c,object2.d);//3,5               //复制

注:

  • 如果只有一个参数,该函数会直接返回该参数

  • 如果传入null和undefined作为参数会报错,因为null和undefined无法被转换为对象

  • 该函数可以用来处理数组,但是会把数组视为对象

  • 可以用扩展运算符(...)来对一个对象进行拷贝

如果想要地址也不同就要进行深拷贝,深拷贝是将重新开辟空间进行复制

//这里是别人直接写的方法,拿过来引用
function deepClone(obj){
    let objClone = Array.isArray(obj)?[]:{};//Array.isArray()函数可以低版本IE不兼容,可以用
    if(obj && typeof obj==="object"){       //obj instanceof Array来判断是否为数组
        for(key in obj){
            if(obj.hasOwnProperty(key)){
                //判断ojb子元素是否为对象,如果是,递归复制
                if(obj[key]&&typeof obj[key] ==="object"){
                    objClone[key] = deepClone(obj[key]);
                }else{
                    //如果不是,简单复制
                    objClone[key] = obj[key];
                }
            }
        }
    }
    return objClone;
}

用JSON进行深拷贝(这种拷贝不支持函数)

var obj={a:1,b:[0,1]};
var json=JSON.parse(JSON.stringify(obj));
json.b.push(2);
console.log(obj);//{a:1,b:[0,1]};
console.log(json);//{a:1,b:[0,1,2]};

兼容深浅拷贝

//obj为拷贝对象,deep是一个布尔值,默认是flase代表浅拷贝,true代表深拷贝
function extend(obj,deep){//因为不传是undefined转布尔值为false
    var objClone=(obj instanceof Array)?[]:{};
    if(obj && typeof obj==="object"){
        for(key in obj){
            if(obj.hasOwnProperty(key)){
                if(!!deep&&obj[key]&&typeof obj[key] ==="object"){
                     objClone[key] = extend(obj[key]);
                 }else{
                      objClone[key] = obj[key];
                  }   
            }
        }
    }
   return objClone;
}

10.12 call,apply与bind方法

这三个方法都是函数对象的方法,需要通过函数对象来使用

  • call()与apply()方法

    当对函数调用call()和apply()方法时都会调用函数执行,在调用call()和apply()可以将一个对象指定为第一个参

    数,此时函数执行时的this将会指向该对象

    function fun(){
        console.log(this);
    }
    var obj={name:"孙悟空"}; 
    var obj2={name:"猪八戒"};
    
    fun.call(obj);//this指向obj
    fun.apply(obj2);//this指向obj2
    fun();//this永远指向window
    
    /*
    如果是在通过一个对象运用该对象的方法时在call()或者apply()中指定了其他的对象,this就会变成其他的对象而不会是原来的对象了
    */
    

    call()与apply()方法的区别

    除了第一个参数,两个方法还可以传入其它的参数,这些参数是要传入指定函数的实参

    • call()方法传递实参只需要在第一个参数后面依次用,(逗号)隔开

      var obj={num:1};
      function fun(number1,number2){
          console.log(this,num+number1+number2);
      }
      fun.call(obj,10,20);//31
      
    • apply()方法需要将实参封装到一个数组中统一传递

      var obj={num:1};
      function fun(number1,number2){
          console.log(this,num+number1+number2);
      }
      fun.apply(obj,[10,20]);//31
      

**小应用:**使用Object.prototype.toString.call(obj)能够判断一个对象是否为某一类型对象

function getFunc(type){
    return function(obj){
        return Object.prototype.toString.call(obj)===type;
    }
}

var fun=getFunc("[object Array]");
var result=fun([10,20,30]);
console.log(result);//true

var fun=getFunc("[object Objcet]");
var dt=new Date();
var result1=fun(dt);
console.log(result1);//false
  • bind()方法
  • bind()方法能够复制一个函数或方法,并将赋值后的函数或方法返回,在赋值时可用传入了对象作为复制的函数 中的this指向(该方法不兼容IE8及以下浏览器) 注意:
    • 传入实参可以在复制的时候传入.也可以在调用的时候传入
    • 与call()和apply()方法不同,使用了bind方法只会将复制后的函数作为返回值传递回来,并不会调用函数
function Person(age){
    this.age=age;
}
Person.prototype.play=function(){
    console.log(this);
    console.log(this.age);
}

function Student(age){
    this.age=age;
}
var per=new Person(10);
var stu=new Student(20)var fun=per.play.bind(stu);
fun();//this指向为stu,this.age的值为20
function f1(x,y){
console.log(x+y)}
var ff=f1.bind(null,10,20)//在复制的时候传入实参
ff();
Var ff=f1.bind(null);
ff(10,20);//在调用的时候传入参数

bind()的应用

function showRandom(){
    //产生1-10的随机数
    this.number=parseInt(Math.random()*10+1);
}
//添加原型方法
showRandom.prototype.show1=function(){
    //改变了定时器中的this执行,本来是window,现在为实例对象
    window.setInterval(this,show2.bind(this),1000);
}
//显示随机数
showRandom.prototype.show2=function(){
    console.log(this.number);
}

var num=new showRandom();
num.show1();

//定时器只有在调用函数里面的时候才会把里面的this改为window,而传了一个关于this的函数其实是先用了这个属性再开的定时器

兼容代码

if(!Function.prototype.bind){
    Function.prototype.bind=function(){
        var bindThis=arguments[0];
       var arg=[].slice.call(arguments,1);
       var that=this;
       return function(){
          that.apply(bindThis,arg);
      }
    }
}

10.13 闭包

  • **闭包的概念:**函数A中,有一个函数B,函数B中可以访问函数A中定义的变量或者数据,此时形成了闭包(这句话暂时不严谨)

  • **闭包的模式:**分为函数模式的闭包和对象模式的闭包(函数中有一个对象)

  • **闭包的作用:**缓存数据,延迟作用域链,闭包使用的父函数的变量或参数会被永久保存,直到页面关闭

  • **闭包的优点和缺点:**缓存数据(优点也是缺点,没有及时的释放数据) 局部变量在函数中被函数使用后会被自动的释放,而闭包后,里面的局部变量的使用作用域链就会被延长

  • 闭包的形成条件: 如果满足以下两点,那么可以把内部函数称为闭包

    • 函数嵌套函数
    • 内部函数使用父函数的变量或参数
//函数模式的闭包
//例一
function f1(num){
    function f2(){
        console.log(++num);//11,因为f2在f1的函数作用域里面,所以f2能用f1传入的参数
    }
    f2()
}
f1(10);//10
//例二2
document.onclick=(function(){
    var a=0;
    return function(){
        console.log("第”+ ++a +“次点击页面");
    }
})()

//对象模式的闭包
function f3(){
    var num=10;
    var obj={
     age:num;   
    }
    console.log(obj.age);
}
f3();//10
//案例:输出三个相同的随机数
function fun(){
    var num=parseInt(Math.random()*10+1)return function(){
        console.log(num);
    }
}
var fun2=fun();
//下面输出的结果全部相同
fun2();
fun2();
fun2();
//案例:输出阶乘
function fun(){
    var obj={};
    return function f(n){
        var num=n+"!";
        if(obj[num]){
            return obj[num];
        }else if(n=1){
            obj[num]=1;
            return 1;
        }else{
            obj[num]=n*f(n-1)
            return obj[num];
        }
    }
}
var fun1=fun();
console.log(fun1(10));
console.log(fun1(9));
console.log(fun1(11));
/*
在计算10的阶乘时会用一定时间,然后会在obj内部储存着1到10的所有阶乘,这时就相当于数据缓存,在计算9和11的阶乘的时候运行速度更快
*/

**总结:**如果想要缓存数据,就把这个数据放在外层函数和里层函数的中间位置

10.14 垃圾回收(GC)

垃圾积攒过多会导致程序运行的速度过慢,所以需要一个垃圾回收机制

当一个对象没有任何的变量或属性对它进行引用,此时我们将永远无法操作该对象,此时这种对象就像一个垃圾,

会占用大量内存空间,导致程序运行变慢,在JS中拥有自动的垃圾回收机制,会自动回收,但是如果有一个变量或

属性还占用着这个对象.这个对象就不会被自动回收,所有我们需要将不再使用的对象设置为null来解放该对象

JS的内存回收机制

一个函数在执行开始的时候,会给其中定义的变量划分内存空间保存,以备后面的语句所用,等到函数执行完毕返回了,这些变量就被认为是无用的了,对应的空间也就被回收了。下次再执行此函数的时候,所有的变量又回到最初的状态,重新赋值使用。

但是如果这个函数内部又嵌套了另一个函数(这就是闭包了),而这个函数是有可能在外部被调用到的。并且这个内部函数又使用了外部函数的某些变量的话.这种内存回收机制就会出现问题。如果在外部函数返回后,又直接调用了内部函数,那么内部函数就无法读取到他所需要的外部函数中变量的值了。

所以js解释器在遇到函数定义的时候会自动把函数和他可能使用的变量(包括本地变量和父级和祖先级函数的变量(自由变量))一起保存起来。也就是构建一个闭包,这些变量将不会被内存回收器所回收,只有当内部的函数不可能被调用以后(例如被删除了,或者没有了指针),才会销毁这个闭包,而没有任何一个闭包引用的变量才会被下一次内存回收启动时所回收。