稳扎稳打JavaScript[转] (一)作用域链内存模型 几个概念 JavaScript的作用域控制机制 (二)图解对象内存模型 1. 什么是JS对象? 2. 如何创建JS对象? 3. 对象创建过程的内存模型 4. 变量查找 与 属性查找 的区别 5. this是在函数执行时赋值的 (三)创建对象的几种方式 1. 工厂模式 2. 构造函数模式 3. 原型模式 4. 构造函数+原型模式 5. 构造函数+原型模式PRO(动态原型模式) 6. 寄生构造函数模式 7. 稳妥构造函数模式 8. 地球人都知道的方式 (四)闭包 什么是闭包 闭包的原理 闭包的特点 闭包的内存问题 稳扎稳打JS——this this的值是在运行时确定的 稳扎稳打JS——“对象” 一切皆“对象” 对象都是由函数创建的 每个函数都有prototype属性 所有对象都有proto属性 稳扎稳打JS——“继承” JS使用原型链实现“继承” 稳扎稳打JS——执行上下文 上下文环境的初始化在代码执行前完成

稳扎稳打JavaScript[转]
(一)作用域链内存模型
几个概念
JavaScript的作用域控制机制
(二)图解对象内存模型
1. 什么是JS对象?
2. 如何创建JS对象?
3. 对象创建过程的内存模型
4. 变量查找 与 属性查找 的区别
5. this是在函数执行时赋值的
(三)创建对象的几种方式
1. 工厂模式
2. 构造函数模式
3. 原型模式
4. 构造函数+原型模式
5. 构造函数+原型模式PRO(动态原型模式)
6. 寄生构造函数模式
7. 稳妥构造函数模式
8. 地球人都知道的方式
(四)闭包
什么是闭包
闭包的原理
闭包的特点
闭包的内存问题
稳扎稳打JS——this
this的值是在运行时确定的
稳扎稳打JS——“对象”
一切皆“对象”
对象都是由函数创建的
每个函数都有prototype属性
所有对象都有proto属性
稳扎稳打JS——“继承”
JS使用原型链实现“继承”
稳扎稳打JS——执行上下文
上下文环境的初始化在代码执行前完成

几个概念

在开始之前,先了解几个概念。

1.1. 作用域

  • 作用域是指当前正在执行的代码能够访问到变量的范围;
  • 每个函数都有各自的作用域,存储函数所有的局部变量;

1.2. 变量对象

  • 变量对象用于存储函数各自的局部变量;
  • 每个函数都有各自的变量对象,并且在函数执行时被创建;
  • 上文提到:“每个函数都有各自的作用域,用于存储函数的局部变量”,其实这句话并不严谨。作用域中存储的其实是变量对象的引用,而变量对象才是存储函数局部变量的地方,就像这样:
    稳扎稳打JavaScript[转]
(一)作用域链内存模型
几个概念
JavaScript的作用域控制机制
(二)图解对象内存模型
1. 什么是JS对象?
2. 如何创建JS对象?
3. 对象创建过程的内存模型
4. 变量查找 与 属性查找 的区别
5. this是在函数执行时赋值的
(三)创建对象的几种方式
1. 工厂模式
2. 构造函数模式
3. 原型模式
4. 构造函数+原型模式
5. 构造函数+原型模式PRO(动态原型模式)
6. 寄生构造函数模式
7. 稳妥构造函数模式
8. 地球人都知道的方式
(四)闭包
什么是闭包
闭包的原理
闭包的特点
闭包的内存问题
稳扎稳打JS——this
this的值是在运行时确定的
稳扎稳打JS——“对象”
一切皆“对象”
对象都是由函数创建的
每个函数都有prototype属性
所有对象都有proto属性
稳扎稳打JS——“继承”
JS使用原型链实现“继承”
稳扎稳打JS——执行上下文
上下文环境的初始化在代码执行前完成

1.3. 作用域链

  • 把多个作用域串起来便形成了作用域链;
  • 每个函数在初始化完成之后就拥有了各自的作用域链,但此时的作用域链中并不包含自己的作用域;只有当函数执行时,才会创建自己的作用域,并加入到作用域链的开头;
  • 作用域链中不仅存储了函数本身的作用域,还存储了该函数能够访问的其他函数的作用域;

1.4. 执行环境

每个正在执行的函数都有一个执行环境,记录了函数执行过程中的各项信息。
除了全局执行环*,其余函数的执行环境都会随着函数的执行而被创建,函数的执行结束而被销毁。
所有的执行环境会存放在执行环境栈中,只有栈顶的执行环境才有执行权。

JavaScript的作用域控制机制

2.1. JS作用域的内存模型

每个函数都有各自的作用域、作用域链、变量对象、执行环境。
其中,作用域链在函数初始化完成后便存在,而作用域、变量对象、执行环境只有在函数被执行时才创建。
执行结束后,函数的作用域、作用域链、执行环境被销毁;而变量对象仍有可能留在内存中(如果函数内部有闭包,则函数执行结束后变量对象仍然留在内存,直到闭包执行结束,该变量对象才会被销毁)。

先来看如下代码:

var 全局变量 = "柴毛毛";

function 外层函数(){
    var 局部变量1 = "大闲人";

    return function(){
        var 局部变量2 = "是傻逼";
        return 全局变量+局部变量1+局部变量2;
    };
}

var 函数 = 外层函数();
函数();

上述代码对应的内存模型如下:

  1. 首先初始化全局执行环境
    稳扎稳打JavaScript[转]
(一)作用域链内存模型
几个概念
JavaScript的作用域控制机制
(二)图解对象内存模型
1. 什么是JS对象?
2. 如何创建JS对象?
3. 对象创建过程的内存模型
4. 变量查找 与 属性查找 的区别
5. this是在函数执行时赋值的
(三)创建对象的几种方式
1. 工厂模式
2. 构造函数模式
3. 原型模式
4. 构造函数+原型模式
5. 构造函数+原型模式PRO(动态原型模式)
6. 寄生构造函数模式
7. 稳妥构造函数模式
8. 地球人都知道的方式
(四)闭包
什么是闭包
闭包的原理
闭包的特点
闭包的内存问题
稳扎稳打JS——this
this的值是在运行时确定的
稳扎稳打JS——“对象”
一切皆“对象”
对象都是由函数创建的
每个函数都有prototype属性
所有对象都有proto属性
稳扎稳打JS——“继承”
JS使用原型链实现“继承”
稳扎稳打JS——执行上下文
上下文环境的初始化在代码执行前完成
    当初始化全局执行环境时,会进行如下操作:

    • 创建全局变量对象。其中包含所有的全局变量,上述代码中分别是“全局变量”和“外层函数”。
    • 创建全局作用域链。该作用域链中只包含一个全局变量对象。
    • 创建外层函数的作用域链。我们知道,函数一旦被初始化后就会创建它的作用域链,只不过这个作用域链中不包含函数本身的作用域,只包含其父级函数的作用域链。这里就是全局作用域。
    • 创建全局执行环境。全局环境通过一个指针指向它的作用域链,作用域链中又通过指针指向它的变量对象。
  2. 调用外层函数时
    稳扎稳打JavaScript[转]
(一)作用域链内存模型
几个概念
JavaScript的作用域控制机制
(二)图解对象内存模型
1. 什么是JS对象?
2. 如何创建JS对象?
3. 对象创建过程的内存模型
4. 变量查找 与 属性查找 的区别
5. this是在函数执行时赋值的
(三)创建对象的几种方式
1. 工厂模式
2. 构造函数模式
3. 原型模式
4. 构造函数+原型模式
5. 构造函数+原型模式PRO(动态原型模式)
6. 寄生构造函数模式
7. 稳妥构造函数模式
8. 地球人都知道的方式
(四)闭包
什么是闭包
闭包的原理
闭包的特点
闭包的内存问题
稳扎稳打JS——this
this的值是在运行时确定的
稳扎稳打JS——“对象”
一切皆“对象”
对象都是由函数创建的
每个函数都有prototype属性
所有对象都有proto属性
稳扎稳打JS——“继承”
JS使用原型链实现“继承”
稳扎稳打JS——执行上下文
上下文环境的初始化在代码执行前完成
    当调用“外层函数”时,会进行如下操作:

    • 创建外层函数的变量对象。变量对象中包含外层函数的全部局部变量,这里分别是“局部变量1”和那个匿名函数。
    • 将当前函数的作用域添加到当前函数作用域链的顶部。也就是把先前创建的“外部函数作用域链”中第一个作用域的指针指向“外部函数变量对象”,第二个指针指向“全局变量对象”。
    • 将“外层函数的执行环境”压入执行环境栈的顶部。PS:执行环境栈顶表示当前正在执行的环境。
  3. 调用闭包时
    稳扎稳打JavaScript[转]
(一)作用域链内存模型
几个概念
JavaScript的作用域控制机制
(二)图解对象内存模型
1. 什么是JS对象?
2. 如何创建JS对象?
3. 对象创建过程的内存模型
4. 变量查找 与 属性查找 的区别
5. this是在函数执行时赋值的
(三)创建对象的几种方式
1. 工厂模式
2. 构造函数模式
3. 原型模式
4. 构造函数+原型模式
5. 构造函数+原型模式PRO(动态原型模式)
6. 寄生构造函数模式
7. 稳妥构造函数模式
8. 地球人都知道的方式
(四)闭包
什么是闭包
闭包的原理
闭包的特点
闭包的内存问题
稳扎稳打JS——this
this的值是在运行时确定的
稳扎稳打JS——“对象”
一切皆“对象”
对象都是由函数创建的
每个函数都有prototype属性
所有对象都有proto属性
稳扎稳打JS——“继承”
JS使用原型链实现“继承”
稳扎稳打JS——执行上下文
上下文环境的初始化在代码执行前完成
    调用闭包时,会进行如下操作:

    • 销毁“外层函数”的作用域链和执行环境。
    • 创建闭包的变量对象。
    • 创建闭包的作用域,并压入闭包作用域链的头部。
    • 创建闭包的执行环境,并指向闭包的作用域链。
      只有当闭包执行结束后,“外层函数的变量对象”才会被释放,否则它将一直驻留内存,因此闭包会比普通函数占用更多的内存,因此要慎用!

2.2. 变量查找

当上述代码执行到“return 全局变量+局部变量1+局部变量2;”时,此时执行环境栈的栈顶是闭包的执行环境,因此通过闭包的作用域链寻找这三个变量的值。
查找过程首先从作用域链的顶部开始,首先在闭包变量对象中寻找“全局变量”的值,若没有,则去外层函数的变量对象中查找;若仍未找到,则去全局变量对象中查找,直到找到为止;若在全局变量对象中仍未找到,则查找失败。
若在某一个变量对象中找到该值,则立即停止查找。
PS:查找过程必须从作用域链的头部开始,依次向后查找。

2.3. JS没有块级作用域

JS没有块级作用域。因此,在if-else、while、switch-case语句中通过var定义的变量都属于当前所在的函数。

2.4. JS作用域类型

严格来说,JS中只有两种作用域:全局作用域 和 函数作用域。
但还有两种特殊的作用域:catch、with。
来看如下代码:

function fn(person) {
    with(person){
        var personInfo = name+age+location;
    }
}

上述代码在函数中使用with语句,with后需要有一个对象,从而在with语句中使用该对象中的属性就不需要通过person.xxx访问,直接访问其属性即可。
并且,JS没有块级作用域,因此在with、catch中创建的变量将属于离它们最近的函数!
那么,这种功能JS是如何实现的呢?

当执行到一个with语句时,会JS会为其创建一个变量对象,这个变量对象中包含with语句后传入的那个对象的全部属性。
紧接着,为with语句创建一个指向该变量对象的作用域,并添加到当前函数/全局作用域链的头部。
当with语句块结束,该变量对象就会被销毁,作用域也会被弹出。
因此,with语句能临时性延长当前函数/全局作用域链的长度,在with语句块中就可以不带前缀访问对象的属性,因为with中传入的对象已经作为一个变量对象被添加到当前作用域链的头部,通过作用域链的查找规则就能找到该变量对象中的属性。

那么with语句块有何用呢?
如果你要大量用到一个对象的属性,重复写person.xxx太繁琐了,这种情况下你可以使用with语句。
但在严格模式下是禁止使用with语句的,只要了解原理就好,平时尽量避免使用。

catch语句块原理一样,请自行脑补吧。

(二)图解对象内存模型

1. 什么是JS对象?

在JS中,对象是一组无序属性的集合。其中,属性可以是基本数据类型、引用类型、函数。如下面这个对象的例子:

var chai={
    name : "柴毛毛", // 属性为基本数据类型
    perosn : { // 属性为引用类型
        address : "xxx",
        sex : "man"
    },
    getName : function () { // 属性为函数
        return this.name;
    }
}

也就是说,JS中的对象类似于Java中的Map,由键值对构成;其中键是字符串类型的属性名,值可以为上述三种类型中的任意类型。

2. 如何创建JS对象?

JS中创建对象的方法有很多,各有千秋。这篇博客主要介绍对象创建过程中的内存模型,因此只介绍通过构造函数创建对象的方法,其余方法期待下一篇博客吧。

2.1. 通过构造函数创建对象的步骤

  • 定义构造函数
function Person (name,age) {
    this.name = name;
    this.age = age;
    this.getName = function(){
        return name;
    }
}
  • 通过new创建对象
var person1 = new Person('柴毛毛',18);

通过上述两步后,就能创建一个Person对象,并由变量person1指向,通过该变量就能访问这个对象的所有属性。

构造函数与普通函数的区别?

3. 对象创建过程的内存模型

以上的概念较为基础,点到为止,为下面讲解对象的内存模型作铺垫,OK,重头戏来了。

3.1. 初始化构造函数

我们知道,构造函数也是函数,所有函数在被执行前都需要被初始化,函数初始化也就是创建该函数对象的过程。那么函数什么时候会被初始化呢?

当构造函数所在的外层函数被执行时,JS引擎会为该外层函数创建一个执行环境,并压入执行环境栈中。这个过程称为函数执行环境的准备阶段。
若构造函数以“函数声明”的方式定义,那么构造函数的初始化在外层函数的环境准备阶段完成;
若构造函数是以“函数表达式”的方式定义,那么只有当JS引擎执行到这一行代码的时候才会初始化构造函数。

我们知道,JS中的函数就是一个对象,因此函数的初始化过程其实就是创建了一个函数对象。

PS:不仅仅是构造函数,所有函数初始化发生的时机都是如此。
综上所述:
函数初始化的时机与函数所在的外层函数有关,也与函数的定义方式有关。
若函数以“函数声明”的形式定义,那么该函数的初始化在其外层函数执行前(即外层函数执行环境的准备阶段)完成;
若函数以函数表达式的形式定义,那么该函数的初始化在其外层函数执行到这行代码的时候完成。

3.2. 创建构造函数的原型对象

当构造函数的对象创建完成后,JS引擎随之会为该对象创建一个原型对象。
原型对象默认只有一个属性“construcor”,指向它的构造函数对象。
而每个函数对象默认都有一个prototype属性,该属性指向它的原型对象。
此时,内存中有如下对象:
稳扎稳打JavaScript[转]
(一)作用域链内存模型
几个概念
JavaScript的作用域控制机制
(二)图解对象内存模型
1. 什么是JS对象?
2. 如何创建JS对象?
3. 对象创建过程的内存模型
4. 变量查找 与 属性查找 的区别
5. this是在函数执行时赋值的
(三)创建对象的几种方式
1. 工厂模式
2. 构造函数模式
3. 原型模式
4. 构造函数+原型模式
5. 构造函数+原型模式PRO(动态原型模式)
6. 寄生构造函数模式
7. 稳妥构造函数模式
8. 地球人都知道的方式
(四)闭包
什么是闭包
闭包的原理
闭包的特点
闭包的内存问题
稳扎稳打JS——this
this的值是在运行时确定的
稳扎稳打JS——“对象”
一切皆“对象”
对象都是由函数创建的
每个函数都有prototype属性
所有对象都有proto属性
稳扎稳打JS——“继承”
JS使用原型链实现“继承”
稳扎稳打JS——执行上下文
上下文环境的初始化在代码执行前完成
PS:不仅仅是构造函数,所有函数初始化完成之后在内存中都有上述结构。
所有函数初始化完成后都会创建一个函数对象和一个原型对象,并且通过prototype、construcor属性相互引用。

3.3. 执行关键字new

var person1 = new Person('柴毛毛',18);

当通过关键字new创建一个对象时,JS引擎会做如下几件事:

  • 创建一个对象
    就像这样:
var object = new Object();

这个object对象将会有一个指向原型对象的属性,它是一个隐式属性,我们无法通过JS代码访问,但JS引擎可以访问。
大多数浏览器将它定义为“proto”。

此时内存中一共存在三个对象,分别是:构造函数本身的函数对象、构造函数的原型对象、构造函数的实例对象。它们之间通过prototype、proto、constructor指针相互引用。
稳扎稳打JavaScript[转]
(一)作用域链内存模型
几个概念
JavaScript的作用域控制机制
(二)图解对象内存模型
1. 什么是JS对象?
2. 如何创建JS对象?
3. 对象创建过程的内存模型
4. 变量查找 与 属性查找 的区别
5. this是在函数执行时赋值的
(三)创建对象的几种方式
1. 工厂模式
2. 构造函数模式
3. 原型模式
4. 构造函数+原型模式
5. 构造函数+原型模式PRO(动态原型模式)
6. 寄生构造函数模式
7. 稳妥构造函数模式
8. 地球人都知道的方式
(四)闭包
什么是闭包
闭包的原理
闭包的特点
闭包的内存问题
稳扎稳打JS——this
this的值是在运行时确定的
稳扎稳打JS——“对象”
一切皆“对象”
对象都是由函数创建的
每个函数都有prototype属性
所有对象都有proto属性
稳扎稳打JS——“继承”
JS使用原型链实现“继承”
稳扎稳打JS——执行上下文
上下文环境的初始化在代码执行前完成
- 准备构造函数的执行环境(变量对象、作用域链、执行环境)
对象创建完后,就要开始执行构造函数中的代码。但在执行函数之前,首先需要准备函数的执行环境:
(本过程详解请参阅:稳扎稳打JavaScript——作用域链
* 创建构造函数的变量对象(用于存储函数执行过程中的所有变量,包括this和arguments)
* 创建构造函数的作用域,压入作用域链的头部,并且指向刚才创建的变量对象;(函数的作用域链在函数初始化时就已创建)
* 创建构造函数的执行环境,并指向该函数的作用域链
此时内存模型如下:
稳扎稳打JavaScript[转]
(一)作用域链内存模型
几个概念
JavaScript的作用域控制机制
(二)图解对象内存模型
1. 什么是JS对象?
2. 如何创建JS对象?
3. 对象创建过程的内存模型
4. 变量查找 与 属性查找 的区别
5. this是在函数执行时赋值的
(三)创建对象的几种方式
1. 工厂模式
2. 构造函数模式
3. 原型模式
4. 构造函数+原型模式
5. 构造函数+原型模式PRO(动态原型模式)
6. 寄生构造函数模式
7. 稳妥构造函数模式
8. 地球人都知道的方式
(四)闭包
什么是闭包
闭包的原理
闭包的特点
闭包的内存问题
稳扎稳打JS——this
this的值是在运行时确定的
稳扎稳打JS——“对象”
一切皆“对象”
对象都是由函数创建的
每个函数都有prototype属性
所有对象都有proto属性
稳扎稳打JS——“继承”
JS使用原型链实现“继承”
稳扎稳打JS——执行上下文
上下文环境的初始化在代码执行前完成
到此为止,内存中存在两大块内容,一块是用于执行构造函数的“构造函数执行环境”,它包括:构造函数的执行环境、构造函数的作用域链、构造函数的变量对象。
另一块内存空间存储了与创建对象相关的内容,包括:构造函数本身的函数对象、构造函数的原型对象、构造函数的实例对象。
但此时这两块内存间并没有联系,接下来this就要出场了。

  • 将变量对象中的this指向实例对象
    此时,构造函数的变量对象中的this指向了构造函数对象,从而使两大块内存之间建立起了桥梁,此时的内存图如下:
    稳扎稳打JavaScript[转]
(一)作用域链内存模型
几个概念
JavaScript的作用域控制机制
(二)图解对象内存模型
1. 什么是JS对象?
2. 如何创建JS对象?
3. 对象创建过程的内存模型
4. 变量查找 与 属性查找 的区别
5. this是在函数执行时赋值的
(三)创建对象的几种方式
1. 工厂模式
2. 构造函数模式
3. 原型模式
4. 构造函数+原型模式
5. 构造函数+原型模式PRO(动态原型模式)
6. 寄生构造函数模式
7. 稳妥构造函数模式
8. 地球人都知道的方式
(四)闭包
什么是闭包
闭包的原理
闭包的特点
闭包的内存问题
稳扎稳打JS——this
this的值是在运行时确定的
稳扎稳打JS——“对象”
一切皆“对象”
对象都是由函数创建的
每个函数都有prototype属性
所有对象都有proto属性
稳扎稳打JS——“继承”
JS使用原型链实现“继承”
稳扎稳打JS——执行上下文
上下文环境的初始化在代码执行前完成
  • 执行构造函数中的代码
function Person (name,age) {
    this.name = name;
    this.age = age;
    this.getName = function(){
        return name;
    }
}

此时,JS引擎将会逐行执行构造函数中的代码。
比如,当代码执行到this.name=name时,JS引擎首先会在当前作用域链中寻找变量this。我们知道,查找变量首先从作用域的头部开始,因此首先寻找下标为0的变量对象;在该变量对象中找到了this,因此查找成功,紧接着在this指向的对象中创建name属性,并赋上局部变量name的值。

当构造函数中的代码执行完毕,内存模型如下:
稳扎稳打JavaScript[转]
(一)作用域链内存模型
几个概念
JavaScript的作用域控制机制
(二)图解对象内存模型
1. 什么是JS对象?
2. 如何创建JS对象?
3. 对象创建过程的内存模型
4. 变量查找 与 属性查找 的区别
5. this是在函数执行时赋值的
(三)创建对象的几种方式
1. 工厂模式
2. 构造函数模式
3. 原型模式
4. 构造函数+原型模式
5. 构造函数+原型模式PRO(动态原型模式)
6. 寄生构造函数模式
7. 稳妥构造函数模式
8. 地球人都知道的方式
(四)闭包
什么是闭包
闭包的原理
闭包的特点
闭包的内存问题
稳扎稳打JS——this
this的值是在运行时确定的
稳扎稳打JS——“对象”
一切皆“对象”
对象都是由函数创建的
每个函数都有prototype属性
所有对象都有proto属性
稳扎稳打JS——“继承”
JS使用原型链实现“继承”
稳扎稳打JS——执行上下文
上下文环境的初始化在代码执行前完成

构造函数创建的那个对象中多出了几个属性。

  • 返回刚创建的对象
    最后,将this指向的那个对象返回。

4. 变量查找 与 属性查找 的区别

先来复习下JS的变量查找过程(本过程详解请参阅:稳扎稳打JavaScript——作用域链)。
仍以上述代码为例:

function Person (name,age) {
    this.name = name;
    this.age = age;
    this.getName = function(){
        return name;
    }
}
var person1 = new Person('柴毛毛',18);

4.1. 变量查找(作用域查找)

我们知道,通过构造函数创建一个对象时,会发生以下几件事:
1. 创建一个object对象
2. 准备构造函数的执行环境
3. 将构造函数的this指向object对象
4. 执行构造函数
5. 返回object对象
也就是说,通过new创建一个对象本质上仍然是把构造函数当作普通函数执行,只不过在构造函数执行前增加了一句var object=new Object();并且在构造函数执行结束后返回这个object;其余过程就是在执行一个普通函数。

那么在函数执行过程中,如果需要用到局部变量,就会发生变量查找。
当执行代码“this.name = name”时,这句代码涉及到两个局部变量:this、name。
以查找this为例,JS引擎会沿着当前函数的作用域链依次查找变量对象。若在某一个作用域指向的变量对象中找到this,则查找成功;否则就继续沿着作用域链向后查找。

4.2. 属性查找(原型链查找)

变量查找是在作用域链上进行,而属性查找是在原型链上进行的。

继续上述例子。当JS引擎在某个变量对象中找到this后,变量查找的过程结束,若查找的是一个基本数据类型的变量,那么查找结束;若查找的变量是一个对象,并且需要对该对象的属性进行操作,那么接下来就要进入属性查找的过程。

我们知道,构造函数执行前会将它的this指向构造函数的实例对象,因此,当执行“this.name”时,JS引擎就会在this指向的实例对象中查找。若在this指向的实例对象中查找不到,就会继续查找该实例对象proto属性指向的原型对象,若该原型对象中没有,则继续查找原型对象的原型对象,直到查找成功或找到Object的原型对象为止。

5. this是在函数执行时赋值的

(三)创建对象的几种方式

在JS中,创建对象的方式有很多,而且各有千秋,我们需要根据实际的业务需求选择不同的方式。

1. 工厂模式

这是创建对象最为简单的方式。

1.1. 定义方式

工厂模式的思想是:将对象构建的过程封装在一个函数中。这个函数能创建一个对象,并为他进行初始化赋值,最后返回这个对象。若要新建对象,调用这个工厂函数即可。

// 工厂函数
function createPerson ( name, age ) {
    var object = new Object();
    object.name = name;
    object.age = age;
    object.getName = function(){
        return object.name;
    }
    return object;
}

1.2. 对象创建方式

// 创建对象
Person perosn = createPerson("柴毛毛", 18);

1.3. 优点

这种模式唯一的优点就是逻辑简单。

1.4. 缺点

  • 创建的对象都是Object类型,没办法知道它的确切类型。
  • 对象中所有的函数都需要重复定义,浪费内存。
  • 没办法创建该类型的公共属性。

2. 构造函数模式

2.1. 定义方式

function Person( name, age ) {
    this.name = name;
    this.age = age;
    this.getName = function(){ 
        return this.name;
    };
}
  • 这种模式需要创建一个构造函数;
  • 构造函数首字母必须大写,这已经成为不成文的规定,目的是为了提高代码的可读性,只要一看到大写的函数那就说明这是一个构造函数;
  • 构造函数中,this就代表即将创建的那个对象;
  • 对象的属性直接绑在this上;
  • 若构造函数中存在闭包,那么闭包中的this仍然指向即将创建的这个对象。

2.2. 对象创建方式

通过new来创建对象:

var person = new Person("柴毛毛",18);
  • JS中的new关键词是专门用来创建对象的,不要以为它多么高级,其实通过new创建对象的过程本质上只是执行了这个构造函数,不过比执行普通函数多加了几个步骤:
    • 在函数执行前创建一个对象,并将函数的this指向这个对象;
    • 然后正常执行函数代码;
    • 最后自动返回这个对象;

2.3. 构造函数模式创建对象的过程

2.3.1. 当外层函数执行结束后:

当构造函数所在的外层函数执行结束后,JS引擎就会为这个构造函数创建两样东西:

  1. 创建一个构造函数本身的函数对象(暂且叫他A)
  2. 创建构造函数的原型对象(暂且叫他B)
    原型对象B中只有一个constructor属性,指向构造函数对象A;A对象中有一个prototype属性,指向对象B。

这个过程结束后,内存中会产生两个与构造函数相关的对象,分别是构造函数对象 和 构造函数的原型对象,他们之间通过属性constructor和prototype相互关联。

2.3.2. 当执行new Person()时:

  1. 创建一个普通对象C,将C对象的proto属性指向原型对象B,将C对象的constructor属性指向构造函数对象A;
  2. 将构造函数中的this指向对象C;
  3. 依次执行构造函数中的代码;
  4. 返回对象C;

这个过程结束后,新对象的创建过程就结束了。

2.4. 优点

可以知道对象的类型。
通过2.3我们知道,实例对象中的constructor属性指向了它的构造函数,proto属性指向了它的原型对象,因此完全可以确定该对象的类型。

  • 确定对象类型的两种方式:
    • (推荐)对象 instanceof 构造函数 (返回true/false)
    • (不推荐)对象.constructor(直接获取其构造函数对象)

2.5. 缺点

  • 对象中所有的函数都需要重复定义,浪费内存。

3. 原型模式

3.1. 什么是原型?

我们已经知道,当一个函数(暂且叫son函数)的外层函数(father函数)执行结束后,JS引擎会为son函数创建两个相关的对象,分别是 son本身的函数对象 和 son的原型对象。并且这两个对象之间通过属性constructor和prototype相互关联。
当通过new创建对象时,这个对象中的constructor属性 和 proto属性分别会指向 函数对象 和 原型对象。

原型模式中的“原型”就是函数的原型对象。

3.2. 原型的特点

  • 每个函数初始化完成后都会创建一个原型对象,即:函数对象和原型对象一一对应;
  • 通过new创建的所有对象,都有一个proto属性指向其原型对象;

3.3. 原型能做什么?

正因为原型对象拥有3.2所提高的两个特点,因此它可以存储某一类型的共有属性和方法。
这样每个对象就不必重复定义函数了,并且能实现某些属性共享。

3.4. 如何往原型中添加属性?

我们知道,每个通过构造函数的函数对象都有一个prototype属性,指向它的原型对象,因此我们可以使用这个属性访问构造函数的原型对象,并为它添加属性和方法,就像这样:

Person.prototype.getName = function(){
    return this.name;
}
Person.prototype.school = "NJUPT";
  • 注意1:虽然对象中也有个proto属性指向原型对象,但这个属性是提供给JS引擎使用的,我们没办法通过代码访问。
  • 注意2:在原型中添加的属性和函数将会被该类型的所有对象共享;
  • 注意3:原型中的函数的this指向???

3.5. 如何使用原型中的属性?

3.5.1. 访问原型属性

在对象中使用原型属性和使用对象的普通属性一样,不需要任何额外操作:

var person = new Person("柴毛毛",18);
alert(person.school);

当访问person.school时,JS引擎首先从从person对象中找,若找不到,就顺着proto属性去原型中找。

3.5.2. 修改原型属性(基本数据类型)

对象无法修改原型中的任何属性!
若原型对象中有school属性,此时执行下列代码,只会在person对象中添加一个school属性,不会对原型属性有任何影响。

person.school = "BJUPT";

当再次访问person.school时,访问到的就是对象中的school。
只有delete person.school后才能再次访问到原型中的school。
PS:将person.school设为null、undefined并没什么卵用,person.school的结果将仍然是person对象中的school。

3.5.3. 修改原型属性(引用类型)

若原型对象中存在引用类型的属性,那么可以通过对象修改它:

Person.prototype.names = ["chai","mao"];
person.names.push("柴毛毛");

此时,原型中的names将变成[“chai”,”mao”,”柴毛毛”],并且该类型所有对象的names都将改变。

3.5.4. 新增原型属性

3.6. 定义方式

使用原型模式,首先得创建一个构造函数,然后在构造函数的原型对象上添加属性。

function Person () {}
Person.prototype.属性名 = 属性值;
Person.prototype.函数名 = function(){
    // ……
}

3.7. 对象创建方式

var person = new Person();

3.8. 优点

由于原型对象被同一类型的所有对象共享,因此可以将函数 和 共享变量 定义在原型上,从而能避免重复创建对象,节约内存空间,并且能实现变量的共享。

3.9. 缺点

也正是因为原型拥有被同一类型的所有对象共享的特点,因此如果将所有属性都定义在原型上,那么就不存在对象的实例属性了。

4. 构造函数+原型模式

通过构造函数创建的对象只有实例属性,没有共享属性;而通过原型模式创建的对象只有共享属性,没有对象的实例属性;因此可以把这两种模式结合起来,取长补短。

4.1. 定义方式

  • 实例属性在构造函数中定义:
    从而每个对象都有各自不同的实例属性值;
  • 共享属性在原型中定义:
    从而函数、需要共享的属性可以被同一类型的所有对象共享。
// 在构造函数中定义实例属性
function Person (name,age){
    this.name = name;
    this.age = age;
}

// 在原型上定义函数
Person.prototype.getName = function(){
    return this.name;
}
Person.prototype.getAge = function(){
    return this.age;
}

// 在原型上定义共享的属性
Person.prototype.school = "NJUPT";

4.2. 对象创建方式

var person = new Person("柴毛毛",18);

4.3. 优点

这种模式结合了构造函数模式和原型模式的优点:

  • 可以知道对象的类型;
  • 可以避免函数对象被重复定义,节约内存;
  • 可以添加共享属性;

4.4. 缺点

  • 这种模式唯一的缺点就是打破了封装性
    在面向对象的语言中,一个类所有的函数和属性都定义在类中;而这种方式定义的“类”中,函数 和 共享属性 都被定义在了“类”的外面,因此破坏了面向对象的封装性。

5. 构造函数+原型模式PRO(动态原型模式)

这种方式就是为了解决上面方法“破坏了封装性”的问题的。

5.1. 定义方式

// 在构造函数中定义实例属性
function Person (name,age){
    this.name = name;
    this.age = age;

    if ( typeof this.getName != "function" ) {
        // 在原型上定义函数
        Person.prototype.getName = function(){
            return this.name;
        }

        Person.prototype.getAge = function(){
            return this.age;
        }

        // 在原型上定义共享的属性
        Person.prototype.school = "NJUPT";   
    }
}
  • 上述方式把一个“类”中所有的属性都在“类”内部定义,因此体现了面向对象的“封装性”。
  • 当创建Person类的第一个对象时,就会执行到这个if语句块;由于此时Person的原型中并没有任何自定义的属性,因此typeof this.getName将返回”undefined”,而不是”function”,因此就会执行if语句块中的代码;这些属性就会被添加到原型对象中;
  • 当第二次创建Person类的对象时,由于原型中已经存在了先前定义的属性,因此typeof this.getName就会返回”function”,从而if语句块就不会执行。
  • 问题1:可以使用this.prototype来给原型添加属性吗?
    绝对不可以!你的代码若要显示访问原型中的属性,只能通过“构造函数.prototype.xxx”去访问,因为prototype属性只存在于构造函数对象中,实例对象中并没有prototype属性。
    此外,实例对象中有一个指向原型对象的proto属性,但这个属性只给JS引擎使用,我们通过代码是调用不到的!这个属性的作用是:当我们通过实例对象调用原型中的属性时,JS引擎就会使用这个指针通往原型对象。
  • 问题2:用this.getName来判断原型对象中是否有getName函数合理吗?是否可以用Person.prototype.getName来判断?
    这里有两个问题,第一个问题:完全合理。第二个问题:完全可以。下面分别解释。
    我们知道,通过构造函数创建对象后,内存中会有三个与之相关的对象:1.构造函数本身的对象A、2.构造函数的原型对象B、3.实例对象C。
    构造函数中的this指向实例对象C,因此当执行this.getName时,JS引擎首先在对象C中找,而C刚创建,肯定不会有getName函数,因此JS引擎通过proto属性去原型B中找。

5.2. 对象创建方式

var person = new Person("柴毛毛",18);
  • 1

5.3. 优点

和原型模式+构造函数模式相比,增加了封装性,更利于理解代码。

5.4. 缺点

一般都用这种方法,完美无缺点!


一般来说,定义“类”就使用原型模式,先前介绍的几种方式都是为了引出原型模式。

下面介绍几种在特殊情况下会用到的定义“类”的模式。

6. 寄生构造函数模式

当我们要给JS原生对象扩充功能的时候,我们可以在原生对象的原型上添加新功能,由于原型上的属性被该类所有对象共享,因此我们就可以在旧对象上使用新功能。但这种做法太危险,假设现在你系统的Array类中没有distinct函数,然后你在Array.prototype上添加了个distinct函数;当你的系统扩展后,也许那个JS库中也向Array.prototype添加了个distinct函数,那你先前添加的那个函数将被覆盖掉,调用这个函数的代码将会出错。因此,不要向原生对象的prototype中添加任何属性!
但如果想要扩展原生对象的功能,就可以使用寄生构造函数模式。

6.1. 定义方式

function MyArray () {
    var array = new Array();

    array.distinct = function () {
        // ……
    }

    array.sort = function () {
        // ……
    }

    return array;
}

定义方式和工厂模式类似:

  1. 手动new一个需要扩展的原生JS对象;
  2. 在这个对象上添加新功能;
  3. 返回这个对象。

6.2. 调用方式

方式一:直接调用

var array = MyArray();

方式二(推荐):使用new

var array = new MyArray();

为了体现它是一个对象创建的过程,增强可读性,因此建议你仍然使用new来创建对象,那有人不禁要问:这个构造函数违背了构造函数的规则呀,不仅显示创建了一个对象,还显示调用了return语句,难道new调用它仍能正确执行吗?
当然可以!通过new执行一个函数时,JS都会首先创建一个对象,再执行函数中的代码,最后如果没有return语句,就会自动return刚才创建的那个对象;如果有return语句,那么就返回return后面指定的对象。
因此,在这里通过new调用MyArray时,JS引擎会创建一个默认的Object对象,并且将它的proto、constructor分别指向MyArray.prototype和MyArray,然后执行自身的代码。谁知在执行自身代码的过程中又创建了个新对象,并且给新对象添加了属性,最后还返回了这个对象。因此,通过这种模式创建的对象,它的proto、constructor仍然指向Array,因此它的类型是Array,而非MyArray。

6.3. 优点

  • 可以在不影响原生对象的基础上增强原生对象的功能
    因为这种模式在原生“类型”的对象上添加新功能,而非在原生“类型”的原型上添加,因此不会对原型造成任何影响。
    此外,由于创建的对象是原生类型,因此又具有原生类型的所有功能。
    这种模式在扩充原生类型的功能方面堪称完美!

6.4. 缺点

  • 没办法知道对象的确切类型,只能知道它的原生对象类型。

7. 稳妥构造函数模式

由于this、new等关键字会导致安全漏洞的发生,因此若系统安全性要求很高的话就不能使用这些语法,此时若要创建对象就得使用稳妥构造函数模式。

7.1. 方法定义

function Person ( name, age ) {
    var object = new Object();
    // object.name = name;
    // object.age = age;
    object.getName = function(){
        return name;
    }
    object.getAge = function(){
        return age;
    }
    return object;
}

注意这里被注掉的两行代码,对于安全级别较高的属性,不能直接挂载到object对象上(因为直接挂载到object上的属性可以被外界随意修改和访问),而是让它们以局部变量的形式出现在构造函数中,若外界要访问和修改,则在object上添加访问和修改的函数,这样就可以在函数中对修改作限制。
我们知道,函数的局部变量存在于函数的变量对象中,一旦函数执行完成,变量对象中的值只能通过函数内部的闭包来访问。

7.2. 对象创建方式

不能通过new创建,只能当作普通函数调用。

var person = Person("柴毛毛",18);
  • 1

8. 地球人都知道的方式

最后为了体现完整性,我还是把最最最普通的几种对象创建方式加上。

8.1. 方法一

var object = new Object();
object.xxx = xxx;
// ……

8.2. 方法二

var object = {
    xxx : yyy,
    zzz : aaa,
    ……
};

这是JS语法糖,是为了写起来方便而添加的新语法本质上仍然是使用new Object创建对象。

(四)闭包

闭包是JS语言的又一大核心,如果要从内存角度充分理解闭包的话,建议大家先预习下先前的几篇讲博客

什么是闭包

定义

闭包是一个能够访问其他函数作用域的函数。

这句话看似拗口,如果读过先前的几篇博客,那理解起来应该不难。下面来解析一下这句话:

  • 首先,闭包是一个函数;
  • 其次,这个函数不仅能访问自己的作用域,更为关键的是它还能访问其他函数的作用域。

换句话说,如果一个函数能访问其他函数作用域中的变量,那么这个函数就叫做“闭包”。

如何创建闭包?

只要在一个函数中再定义一个函数,这个内部函数就是一个闭包。

注意:只要满足一个函数在另一个函数的内部的条件,这个内部函数就是闭包,不管这个内部函数是以怎样的形式存在于外层函数中的。下面举几个例子:

  • 形式一:
function father () {
    function son () {
    }
}
  • 形式二:
function father () {
    var object = new Object();
    object.son = function () {
    }
}
  • 形式三:
function father () {
    var object = {
        son : function(){}
    };
}

以上三种形式定义的内部函数都是闭包。

  • 形式一:在一个函数中又定义了个函数,这完全符合闭包的定义,不用多说。
  • 形式二:在一个函数中先创建了一个对象,然后在对象内部定义了函数,这也是闭包。
  • 形式三:本质上和形式二一样,也是先在函数内部定义了一个对象,再在对象内部定义了个闭包。只不过定义对象的方式和形式二有所区别。

闭包有何用?

前面闭包的定义中说“闭包能访问其他函数作用域中的变量”。“其他函数”指的是闭包所在的外层函数。
也就是说,闭包能访问它所在外层函数全部变量,即使外层函数已经执行结束。
闭包的这种特性有助于我们在JavaScript这门非面向对象的语言中实现面向对象的一些特性。这个后面会详细介绍。

闭包的原理

闭包的原理涉及到作用域链的内存模型,这里带大家回顾下作用域链,更为详细的内容请看:稳扎稳打JavaScript(一)——作用域链

以下列代码为例:

function father () {
    var name = "爸爸";
    var company = "Google";
    function son () {
        var name = "儿子";
        var school = "启东中学";
        alert(name);
        alert(school);
    }
    return son;
}

这段代码在window作用域中定义了函数father;
father中有两个局部变量:name、company;
father中定义了一个闭包;
闭包中定义了两个局部变量:name、school。

  • 当JS初始化完成后:
    内存中出现与全局环境相关的四个东西:

    1. 全局环境的变量对象(用于存储全局环境的变量,比如:father函数)
    2. 全局环境的作用域 和 作用域链(作用域链是一个链表,由多个作用域构成,全局环境的作用域链中只有一个全局环境的作用域,它指向全局环境的变量对象)
    3. 全局环境的执行环境 和 执行环境栈(执行环境栈里面存放着一个个执行环境,栈顶的那个表示正在执行的环境。执行环境中有一个指针指向它的作用域链)
    4. father函数的作用域链(此时father函数的作用域链中只包含全局环境的作用域,并没有father函数自己的作用域)
      注意:每个函数自己的作用域是在函数执行时才创建的,而函数的作用域链则是在函数所在的环境被执行是创建的。当函数被执行时,它自己的作用域才会被添加到已经创建的作用域链的头部。
  • 当执行father函数时:
    JS引擎也会为它创建上述四样东西:

    1. 一个属于它的变量对象(里面存着name、company、son)
    2. 一个属于它的作用域链
      此时,这个作用域链中包含两个作用域,分别是:全局作用域 和 father函数的作用域,他们分别指向各自的变量对象;
    3. 一个属于他的执行环境,并将执行环境压入执行环境栈的栈顶;
    4. son函数的作用域链(其中包含father和全局环境的作用域)
      此时,内存图如下:
      稳扎稳打JavaScript[转]
(一)作用域链内存模型
几个概念
JavaScript的作用域控制机制
(二)图解对象内存模型
1. 什么是JS对象?
2. 如何创建JS对象?
3. 对象创建过程的内存模型
4. 变量查找 与 属性查找 的区别
5. this是在函数执行时赋值的
(三)创建对象的几种方式
1. 工厂模式
2. 构造函数模式
3. 原型模式
4. 构造函数+原型模式
5. 构造函数+原型模式PRO(动态原型模式)
6. 寄生构造函数模式
7. 稳妥构造函数模式
8. 地球人都知道的方式
(四)闭包
什么是闭包
闭包的原理
闭包的特点
闭包的内存问题
稳扎稳打JS——this
this的值是在运行时确定的
稳扎稳打JS——“对象”
一切皆“对象”
对象都是由函数创建的
每个函数都有prototype属性
所有对象都有proto属性
稳扎稳打JS——“继承”
JS使用原型链实现“继承”
稳扎稳打JS——执行上下文
上下文环境的初始化在代码执行前完成
  • 当father函数执行结束:
    当函数执行结束后,它的执行环境 和 作用域链 将会被销毁,而它的变量对象就取决于是否有引用指向它。
    由于son函数的作用域链指向了father的变量对象,因此它仍然驻留在内存中。

  • 当执行son函数时:
    同样的,JS引擎也会为son函数依次创建这些东西,并将新建的son函数作用域压入作用域链的头部,此时内存图如下:
    稳扎稳打JavaScript[转]
(一)作用域链内存模型
几个概念
JavaScript的作用域控制机制
(二)图解对象内存模型
1. 什么是JS对象?
2. 如何创建JS对象?
3. 对象创建过程的内存模型
4. 变量查找 与 属性查找 的区别
5. this是在函数执行时赋值的
(三)创建对象的几种方式
1. 工厂模式
2. 构造函数模式
3. 原型模式
4. 构造函数+原型模式
5. 构造函数+原型模式PRO(动态原型模式)
6. 寄生构造函数模式
7. 稳妥构造函数模式
8. 地球人都知道的方式
(四)闭包
什么是闭包
闭包的原理
闭包的特点
闭包的内存问题
稳扎稳打JS——this
this的值是在运行时确定的
稳扎稳打JS——“对象”
一切皆“对象”
对象都是由函数创建的
每个函数都有prototype属性
所有对象都有proto属性
稳扎稳打JS——“继承”
JS使用原型链实现“继承”
稳扎稳打JS——执行上下文
上下文环境的初始化在代码执行前完成

  • 当执行alert(company)时:
    JS引擎会沿着son函数的作用域链依次查找变量对象中的值。
    查找首先从son变量对象中开始,若该变量对象中没有,则沿着作用域链继续查找father变量对象,一旦找到,就停止。
    因此,若查找name,首先就在son变量对象中找到,查找停止,因为无法访问到father中的name,除非在son函数中是否delete name将son中的name删除。

闭包的原理总结

综上所述,闭包之所以能访问其外层函数作用域中的变量,是因为闭包的作用域链中存在外层函数的变量对象。即使外层函数之行结束,但由于其变量对象仍然被内层函数的作用域引用,因此不会被内存回收,直到闭包执行结束后,外层函数的变量对象才会被回收。

闭包的特点

闭包访问外层函数变量的特点

若闭包在外层函数执行结束后执行,那么它只能获取到外层函数中所有变量的最终状态。

  • 例1:
function father(){
    var array = [];
    for (var i=0; i<10; i++) {
        array[i] = function(){
            return i;
        }
    }
    return array;
}

father函数执行后会返回一个包含闭包的数组,每个闭包都会返回i。
由于这里的闭包调用时,外层函数早就执行结束了,外层函数变量对象中i值已经变成了9,此时不管执行array中的哪个闭包,返回的结果都是9。
但是,如果在闭包外层函数执行过程中立即执行闭包,那么结果就不一样了,请看例2.

  • 例2:
function father(){
    var array = [];
    for (var i=0; i<10; i++) {
        array[i] = (function(){
            return i;
        })();
    }
    return array;
}

此时的闭包在外层函数执行时就立即执行,在那个时刻,闭包中i的值就是外层函数当前i的值,因此返回的array中存储的将是0-9.
但是,例子1中array存放的明明是闭包,而这里的array存放的却是闭包执行的结果,那么若仍想让array存储闭包,我们还需要稍稍改造,请看例3。

  • 例3:
function father(){
    var array = [];
    for (var i=0; i<10; i++) {
        array[i] = (function(){
            var curI = i;
            return function(){
                return curI;
            }
        })();
    }
    return array;
}

我们让立即执行的闭包再返回一个闭包,并且将for循环中的i值赋给立即执行函数的局部变量,此时array中存储的将是闭包,并且每个闭包都拥有正确的值。

闭包中的this指向window

闭包的内存问题

闭包占用内存比普通函数大

因为如果一个函数内部有闭包存在,那么函数之行结束后不会释放自己的变量对象,只有当闭包执行结束后才会释放,因此闭包将会占用更大的内存空间,用于存储外层函数的变量兑现。
因此,能避免的情况下就不要使用闭包。

闭包中涉及DOM对象可能会出现内存泄漏

我们知道,JavaScript由ECMAScript、BOM、DOM组成,在某些浏览器中他们使用不同的语言实现,因此他们具有不同的垃圾回收机制。
ECMAScript对象采用标记清除算法回收内存,而某些浏览器的DOM对象采用引用计数算法回收内存。引用计数有个致命的缺点——无法回收循环引用的对象。
举个例子:如果一个DOM对象A中的属性a指向另一个DOM对象B,而B中有属性b指向对象A,那么这两个对象存在循环引用,垃圾回收机制就无法回收他们,这就造成了内存泄漏。
退一步将,只要循环引用的两个对象中存在一个DOM对象,就会导致内存泄漏,请看下面的例子:

function func () {
    var dom = document.getElementById("xx");
    dom.onclick = function(){
        alert(dom.id);
    }
}

这段代码获取了一个DOM对象,并且让这个对象的onclick属性指向了一个JS函数对象;而这个函数对象又指向了DOM对象的id属性,从而出现了循环引用,即使func函数执行完成,这个函数中定义的。由于这两个对象中存在一个DOM对象,因此就会出现内存泄漏。

如何避免?

要解决内存泄漏,我们只要破坏两个对象的相互引用即可。
上述代码要为dom添加一个点击事件,因此dom.onclick属性必须要指向一个JS函数对象,因此这个引用不能切断。
而第二个引用是由JS函数对象指向DOM对象的,目的是为了获取dom的id,我们可以通过如下代码切断这个引用:

function func () {
    var dom = document.getElementById("xx");
    var id = dom.id;
    dom.onclick = function(){
        alert(id);
    }
}

稳扎稳打JS——this

this的值是在运行时确定的

JS中的this究竟代表什么,这是在程序运行时根据上下文环境确定,可以分为以下几种情况。

1. 全局作用域中的this

在全局作用域中,this指向window对象。

console.log(this);//指向window对象

this.x = 5//在全局作用域内创建一个x
//与this.x = 5的等价情况:
//var x = 5;
//x = 5;
  • 在全局作用域中执行var x=5,其实是为window对象创建一个属性x,并令其等于5。

  • 若定义变量时不加var,JS会认为该变量为全局变量,会将其当作window对象的属性。

2. 函数中的this

JS中函数有两种,直接调用的函数称为普通函数,通过new创建对象的函数称为构造函数。

2.1 构造函数中的this

构造函数的this指向它所创建的对象,如:

function Person(name){
    this.name = name;//this指向该函数创建的对象person
}
var person = new Person("chaimm");

2.2 普通函数中的this

普通函数的this指向window对象。
若上述例子,直接执行Perosn函数,则其中this代表window对象,因此该函数执行后会创建一个全局的name。

function Person(name){
    this.name = name;//this指向window
}
Person("chai");//当作普通函数执行,this指向window对象

3. 对象中的this

对象中的this指向当前对象,如:

var person = {
    name : "chaimm",
    getName : function(){
        return this.name;
    }
}

上述代码中this指向函数getName所属的对象。
但是,如果一个对象的函数中又嵌套了一个函数,这个函数的this指向的却是window,而并不是其外层的对象。

var person = {
    name : "chaimm",
    setName : function(name){
        (function(name){
            this.name = name; //此时this并不代表person对象,而是代表window对象
        })(name);
    }
}

上述示例中,person对象中有一个getName函数,而getName函数内部又有一个函数,这个函数内部的this指向window对象,而非person对象,这是JS的一个bug!一般作如下处理,规避这个bug:

var person = {
    name : "chaimm",
    setName : function(name){
        var thar = this;//将this赋给that
        (function(name){
            that.name = name;//此时that指向person对象
        })(name);
    }
}

我们在person对象的第一层函数中,将this赋给局部变量that,然后在第二层函数中使用that,此时that指向person对象,可对person的属性进行操作。

  • 注意:若将一个对象中的函数赋给一个变量后,再通过该变量调用这个函数,此时该函数中的this指向window,而非该对象,如下所示:
var person = {
    name : "chaimm",
    getName : function(){
        return this.name;
    }
}

//将getName函数赋给一个新的变量
var newGetName = person.getName;
//通过新的变量调用这个函数,这个函数中的this将指向window
newGetName();//若全局作用域中没有name,则将返回undefined

4. 用call和apply函数给this开挂

这两个函数都能手动指定被调用函数内部的this指向哪个对象。

//定义一个构造函数
var Person = function(name){
    this.name = "";
    this.setName = function(name){
        this.name = name;
    }
}

//创建两个对象
var personA = new Person("A");
var personB = new Person("B");

//使用personA的setName函数去修改personB的name属性
personA.setName.apply(personB,["C"]);
    • apply用法
      对象A.函数名.apply(对象B, 参数列表);
      当对象B作为apply的第一个参数传给apply时,对象A的函数中this就指向了对象B,此时对象A的该函数对this的操作将会作用在对象B上,由此实现了用对象A去调用对象B的函数。

稳扎稳打JS——“对象”

一切皆“对象”

  • JS中一切皆“对象”

  • “对象”是属性的集合,而属性又是对象。既然属性又是对象,那么一个对象的属性也可以拥有属性,如:

//定义一个函数getName
var getName = function(){
    return "chaiMaoMao";
}

//由于一切皆对象,故getName也为对象,故可以给它添加属性,属性可以是任何类型
getName.value = "lalala";
getName.toString = function(){
    //……
}
  • JS中的“对象”只有属性,属性是一组键值对,键表示属性的名字,值表示属性值,属性值可以是任何JS类型(String、Number、Boolean、Object、Array、Function)

  • JS是“基于对象”语言,这意味着它并不是面向对象语言,它没有对象,但可以使用JS的特性模拟面向对象。

  • JQuery中的$其实是是一个函数,我们常看到的$(“#id”)其实是调用函数$,并传递参数 “#id”。

  • JQuery为函数还定义了一些属性,如.trim();


对象都是由函数创建的

  • JS中所有对象都由函数创建
//通过构造函数创建对象
var person = new Person();
//使用JSON创建对象
var person = {
    name : "chaiMaoMao",
    age : 12
}

//JSON创建对象是一种语法糖,该过程相当于
var person = new Object();
person.name = "chaiMaoMao";
person.age = 12;
//创建数组
var array = [1,2,3,4];

//该过程实则为:
var array = new Array(1,2,3,4);
//定义一个数字
var a = 3;

//该过程实则为:
var a = new Number(3);
//定义一个函数
var getName = function(){
    //……
}

//该过程实则为:
var getName = new Function();

由此可见,所有对象都是由函数创建的!


每个函数都有prototype属性

  • prototype属性用于存储函数的属性集合。

  • 初始情况下,一个函数的prototype属性中只含有一个constructor对象,指向函数本身。

  • 存储在函数中的属性值和存储在函数prototype属性中的属性值均能访问,并且访问方式相同,如下:

//定义一个函数Person(属性值存储在函数中)
var Person = function(){
    //……
}
Person.name = "chaimm";
Person.getName = function(){
    //……
}

//访问函数中的属性
var p = new Person();
p.name;
p.getName();
//定义一个函数Perosn(属性值存储在函数的prototype属性中)
var Person = function(){
    //……
}
Person.prototype.name = "chaimm";
Person.prototype.getName = function(){
    //……
}

//访问函数prototype中的属性
var p = new Person();
p.name;
p.getName();

由此可见,访问存储在函数prototype中的属性和访问存储在函数中的属性的方式是一样的。
虽然访问方式一样,但两者存在巨大的差别。

  • 当通过函数创建对象时,对象所有的属性值将会被复制给新的对象。但由于prototype是一个引用类型的变量,存储的是对象的地址,因此prototype中的所有属性值将会被该函数创建的所有对象共享,若一个对象修改了prototype中的某个属性,那所有对象prototype属性都会发生修改。
    因此,prototype属性适合存储不变的属性,如函数;函数的普通属性适合存储基本类型类型的属性;而对象无论存储在哪里效果都一样。

  • 此外,将函数存储在prototype中可以达到节约内存的功效,不论创建多少对象,该对象的函数在内存中只有一份。但若将函数存在构造函数的属性中,那么每次new一个对象,都会拷贝一份函数体,从而造成浪费。


所有对象都有proto属性

  • 所有对象都有私有属性proto,它指向创建该对象的构造函数的prototype属性。

  • JS中所有属性都是共有的,但有个约定,若属性两端加上“_”,表示该属性不希望调用者使用,相当于是“私有属性”。

  • 通过Object创建的对象的proto属性指向Object.prototype

var p = new Object();
  • 通过构造函数创建的对象的proto属性指向该函数的prototype属性
var p = new Person();
    • 所有的函数均由Function创建,因此函数的proto属性指向Function.prototype。

    • Function.prototype._proto_ === Object.prototype
      Function.prototype本身也是个对象,它的_proto_属性指向Object.prototype。

    • Object._proto_ === Function.prototype
      Object是一个构造函数,既然是函数,那么它的_proto_指向Function.prototype

    • Objecet.prototype._proto_ === NULL

    • Function._proto_ === Function.prototype
      Function的_proto_指向创建Function的函数的prototype,Function由它自己创建

稳扎稳打JS——“继承”

JS使用原型链实现“继承”

  • JS是“基于对象”的语言,因此没有继承。但可以使用JS的特性实现“继承”——原型链。

  • JS使用_proto_属性构造原型链,如
    p._proto_ ——> Perosn.prototype
    Perosn.prototype._proto_ ——> Object.prototype
    Person._proto_ ——> Function.prototype
    Function.prototype._proto_ ——> Object.prototype

  • 若当前对象中没有要访问的属性,JS就会自动沿着原型链向上查找,若在某一个对象的prototype中找到相应的属性,则访问它;若直到原型链顶端也没找到,则返回undifined。

//定义构造函数Person
var Person = function(name, age){
    this.name = name;
    this.age = age;
}
Person.prototype.school = "NJUPT";
var p = new Person("chaimm",23);
p.school;

当访问p.school时,JS会首先在对象p的属性中查找,若不存在,则通过_proto_属性向上查找,当发现Person.prototype属性中存在school时,则查找成功。否则,继续沿着原型链向上查找。

  • 覆盖“父类”属性
var p = new Person("chaimm",23);
p.school = "NJU";
alert(p.school);

按照原型链查找次序,首先从当前对象的属性域查找,若找到school,则输出结果。从而实现面向对象中继承的效果。

  • 为“父类”添加新的属性
Person.prototype.hobby = "LOL";

可以为任何“父类”添加任何属性。

  • 使用hasOwnProperty函数可判断某属性是否是本对象的属性
for(item in p){
    if(p.hasOwnProperty(item)){
        console.log(item);
    }
}

hasOwnProperty是Object.prototype中的函数,因此所有对象都可以使用。

稳扎稳打JS——执行上下文

上下文环境的初始化在代码执行前完成

  • JS有三种作用域:全局作用域、函数作用域、eval作用域(不常用,不做介绍)。

  • 在JS代码执行前,首先会对这三种作用域进行上下文环境的准备工作,准备内容如下:

    1. 全局作用域的上下文准备工作
      • 将全局变量设为undefined
      • 将函数表达式的值设为undefined
      • 为函数声明直接赋值
      • 将window对象赋给this
    2. 函数作用域的上下文准备工作
      • 确定*变量的作用域
      • 为函数的参数和arguments对象赋值
      • 将局部变量的值设为undifined
      • 将函数表达式的值设为undifined
      • 为函数声明直接赋值
//将全局变量的值设为undefined
console.log(a); //undefined
var a = 1;

//将window对象赋给this
console.log(this);

//将函数表达式的值设为undefined
console.log(fn1);//undefined
var fn1 = function(){
    //……
}

//为函数声明直接赋值
console.log(fn2); //输出函数的代码
function fn2(){
    //……
}

//函数作用域
function fn3(b,c){
    //确定*变量的作用域
    console.log(a);

    //为局部变量赋上undefined
    console.log(x);//undefined
    var x = 123;

    //为函数的参数赋值
    console.log(b,c); //2,3

    //为arguments赋值
    console.log(arguments); //[2,3]
}

fn3(2,3);
  • 函数作用域中this的是在JS运行时才能确定,而全局作用域中的this在准备上下文环境的时候就确定了(window)。

  • 定义函数有两种方法:函数声明式定义、函数表达式定义,如下所示:

//函数声明式定义
function fn(a,b){
    //……
}

//函数表达式定义
var fn = function(a,b){
    //……
}
    • 两者区别如下:

      • 若采用函数声明式定义,函数调用语句和函数声明语句的先后次序无关。
        因为在代码运行前的准备上下文环境过程中,函数的初始化已经完成,所以运行时无论函数调用语句在哪儿,JS编译器都能调用这个函数。

      • 若采用函数表达式来定义函数,则函数定义一定要在函数调用语句之前!
        因为在这种方式中,函数的初始化是在运行时完成,在代码运行前只是将undefined赋给函数变量。因此在这种情况下,函数调用语句必须在函数定义后。

    • JS中作用域只有三种:全局作用域、函数作用域、eval作用域。除此之外就没有作用域了!如:for、if、while等都不是独立的作用域! 因此在里面定义的局部变量都属于其所属的外层作用域。