重读《JS高程》重新理解JavaScript
一、JavaScript 包含什么
完整的 JavaScript 实现由三部分组成:核心(ECMAScript),文档对象模型(Document Object Model),浏览器对象模型(Browser Obejct Model)。
二、HTML 中引入 JavaScript 脚本
延迟脚本 defer。脚本会延迟直到浏览器遇到</html>标签再执行。异步脚本 async。不让页面等待脚本下载和执行,从而异步加载其他页面内容。把需要延迟的脚本放在页面底部仍然是最佳选择。
混杂模式和标准模式
三、基本概念
3.1、变量
ECMAScript 的变量是松散类型的,即变量可以保存任何类型的数据。
每个变量仅仅是一个用于保存值的占位符而已。
使用 var、const、let 操作符声明,后跟变量名(即一个标识符),若未给其初始化值则默认会保存特殊值 undefined。
注:若直接使用未声明的变量,浏览器会报错 xxx is not defined。
3.2、数据类型
5种基本数据类型:Undefined、Null、Boolean、Number、String。
1种复杂数据类型:Object。
监测数据类型的 typeof 操作符返回的结果:undefined、boolean、number、string、object、function。因为 null 值表示一个空对象指针,所以 typeof null 返回 "object" 。
意在保存对象的变量可先给其初始化为null值。
因为 undefined 值是派生自 null 值的,因此 null == undefiend 返回 true。
3.2.1、Boolean 类型
3.2.2、Number 类型
3.2.3、String 类型
3.2.4、Object 类型
3.3、函数
3.4、操作符
3.5、语句
四、变量值,作用域(变量对象)和内存问题(垃圾回收)
4.1、变量的值
变量可能包含两种不同数据类型的值:基本类型值和引用类型值。
5种基本类型值:undefined、null、Boolean类型值、Number类型值、String类型值。
引用类型值:Object类型值,Function类型值,Array类型值。
基本类型值是存储在栈内存中的简单数据段,变量和变量值都是存储在栈内存中,访问变量即可直接访问其值。
引用类型值是存放在堆内存中的对象。变量存储在栈内存中,而变量值存储在堆内存中,访问变量是访问变量值的引用。
var name = 'zhang' var obj1 = { gender: 'male' } var obj2 = obj1
4.2、执行环境和变量对象
4.3、作用域
五、原生引用类型
引用类型可比作是传统面向对象语言中的类,他描述的是一类对象所具有属性和方法,因此也可以叫做对象定义。
ECMAScript提供了许多原生引用类型,包括:Object, Array, Date, RegExp, Function, (Boolean, Number, String,) Global, Math
引用类型值(引用类型的实例对象)是某个特定引用类型的实例。例如:var person = new Object()
5.1、Object 类型
5.2、Array 类型
5.3、Date 类型
5.4、RegExp 类型
5.5、Function 类型
5.6、基本包装类型
为了便于操作基本类型值,ECMAScript提供了三个特殊的引用类型。我们说基本类型值不是对象,我们不可以给其添加属性和方法。那为什么可以调用字符串上的方法呢?
var s1 = 'hello'
var s2 = s1.substring(2) // 输出 llo
实际上,每当读取一个基本类型值的时候,ECMAScript 规定要创建一个对应的基本包装类型的对象,从而让我们可以调用一些方法来操作数据。
上面第二行在读取 s1 的时候,后台会自动完成如下处理:
- 创建 String 类型的一个实例;
- 在实例上调用指定的方法;
- 销毁这个实例。
可以想象成执行了下列 ECMAScript 代码。对于 Boolean 和 Number 类型值也是如此。
var s1 = new String('hello') var s2 = s1.substring(2) s1 = null
引用类型和基本包装类型的主要区别就是对象的生命周期。使用 new 操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁。所以我们不能在运行时为基本类型值添加属性和方法。
var s1 = 'hello' s1.color = 1 console.log(s1.color) // 输出 undefined
第二行代码试图为字符串 s1 添加一个 color 属性。但是当第三行代码再次访问 s1 时,它的 color 属性不见了。问题原因就是第二行创建的 String 对象在执行到第三行时已经被销毁了。第三行代码又创建了自己的 String 对象,而该对象没有 color 属性。
当然可以显式的调用基本包装类型方法来创建对象
var b = new String('world') b.other = 2 console.log(b.other) // 输出 2 console.log(typeof b) // 输出 object
Object 构造函数也可以根据传入值的类型返回相应的基本包装类型的实例。
var obj = new Object('some') console.log(obj instanceof String) // 输出 true
5.6.1、Boolean 类型
5.6.2、Number 类型
5.6.3、String 类型
5.7、单体内置对象
5.7.1、Global 对象
5.7.2、Math 对象
六、面向对象的程序设计 ⭐️
6.1、理解对象
6.2、创建对象的7种模式(原型)
6.2.1、工厂模式
用函数来封装以特定接口来创建对象的细节。
示例代码中“特定接口”指的是 Obejct() 原生构造函数
function createPerson (name, age) { const o = new Object() o.name = name o.age = age o.sayName = function () { console.log(this.name) } return o } var person1 = createPerson('Tom', 27) var person2 = createPerson('Bob', 30) person1 instanceof createPerson // false person2 instanceof createPerson // false person1.sayName === person2.sayName // false
优点
解决了创建多个相似对象的问题。
缺点
无法识别对象的类型。
6.2.2、构造函数模式
构造函数可用来创建特定类型的对象,如原生构造函数 Object 和 Array。自定义构造函数,可定义【自定义对象类型】的属性和方法。
function Person (name, age) { this.name = name this.age = age this.sayName = function () { console.log(this.name) } } var person1 = new Person('Tom', 27) var person2 = new Person('Bob', 30)
与工厂函数的区别:
- 没有显式地创建对象;
- 直接将属性和方法赋值给了 this 对象;
- 没有 return 语句。
以 new 操作符调用构造函数经历的4个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此 this 指向了这个新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
person1 和 person2 这两个对象的 constructor(构造函数)属性都指向 Person。
person1 instanceof Object // true person1 instanceof Person // true person2 instanceof Object // true person2 instanceof Person // true
优点
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定类型,这正是构造函数模式胜过工厂模式的地方。
缺点
每个方法都要在每个实例上重新创建一遍。
person1.sayName == person2.sayName // false
创建两个完成相同任务的 Function 实例没有必要,况且有 this 对象存在,可将把函数定义转义到构造函数外部来解决。
function Person (name, age) { this.name = name this.age = age this.sayName = sayName } function sayName () { console.log(this.name) } var person1 = new Person('Tom', 27) var person2 = new Person('Bob', 30)
构造函数内 sayName 包含的是一个指向函数的指针,因此 person1 和 person2 对象就共享了在全局作用域中定义的同一个 sayName() 函数。
但是这样缺点也很明显:
全局作用域中的函数只能被某个对象调用,使得全局作用域名不副实;
如果需要定义很多方法,就要定义很多全局函数,那这个自定义的引用类型就丝毫没有封装性可言了。
6.2.3、原型模式![](https://img2018.cnblogs.com/blog/1196628/201909/1196628-20190926102937940-1099372921.png)
6.2.3-1
每个函数都有一个 prototype(原型) 属性,是一个指针,指向一个对象,它是[通过调用构造函数]而创建的[那个对象实例]的[原型对象]。使用原型对象的好处是可以让所有对象实例可共享它所包含的属性和方法。即不必在构造函数中定义实例信息,而是可以将这些信息直接添加到原型对象中。
function Person() {} Person.prototype.name = 'Tom' Person.prototype.age = 27 Person.prototype.sayName = function () { console.log(this.name) } const person1 = new Person() person1.sayName() // Tom const person2 = new Person() person2.sayName() // Tom person1.sayName === person2.sayName
6.2.3-2
prototype 原型对象中默认存在一个 constructor 构造函数属性,它包含一个指针,指向 prototype 所在的函数。
6.2.3-3
当调用构造函数创建一个实例,该实例内部包含一个指针 __proto__ ,指向其构造函数的原型对象。这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。通过 isPrototypeOf() 或者 Object.getPrototypeOf() 可以确认该关系。
Person.prototype.isPrototypeOf(person1) // true Object.getPrototypeOf(person1) === Person.prototype // true
6.2.3-4
我们可以通过对象实例访问保存在原型对象中的值,但却不能通过对象实例重写原型中的值。如果在实例对象中添加了一个与原型对象中同名的属性,那将会在实例中创建该属性,并且会屏蔽原型中的那个同名属性。
使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中还是存在于原型中。存在于实例中才返回 true。
6.2.3-5
如果使用对象字面量重写整个 prototype ,一定要记着重新指定构造函数属性。
function Person() {} Person.prototype = { constructor: Person, // 需重新指定 name: 'Tom', age: 27, sayName: function () { console.log(this.name) } }
6.2.3-6
原型的动态性。先创建实例后,再修改原型中的单个属性,实例也可以访问的新修改的属性。但是如果创建实例后整个替换原型对象,那是访问不到的,即使指定了 constructor 属性。
创建实例后单独修改原型对象某个属性:
function Person() {} Person.prototype = { constructor: Person, name: 'Tom', age: 27, sayName: function () { console.log(this.name) } } const person1 = new Person() Person.prototype.name = 'Bob' person1.sayName() // Bob
创建实例后重写整个原型对象:
function Person() {} const person1 = new Person() Person.prototype = { constructor: Person, name: 'Tom', age: 27, sayName: function () { console.log(this.name) } } person1.name // undefined person.sayName() // error
6.2.3-7
所有原生引用类型(Object、Array、String 等等)都在其构造函数的原型上定义了方法。例如: Array.prototype.sort() 、 String.prototype.substring() 。
我们也可以修改原生对象的原型,添加自定义方法。
String.prototype.startsWith = function (text) { return this.indexOf(text) === 0 } const msg = 'Hello World' msg.startsWith('Hello') // true
6.2.3-8
缺点
原型中的所有属性是被很多实例共享的,对于函数非常合适,对于基本类型值属性也尚可,毕竟在实例上重写属性可屏蔽掉原型属性。但是对于引用类型值属性来说,问题就大了。
function Person() {} Person.prototype = { constructor: Person, name: 'Tom', age: 27, friends: ['ming', 'hong'], sayName: function () { console.log(this.name) } } const person1 = new Person() const person2 = new Person() person1.friends.push('guo') console.log(person1.friends) // ["ming", "hong", "guo"] console.log(person2.friends) // ["ming", "hong", "guo"] console.log(person1.friends === person2.friends) // true
6.2.4、组合使用构造函数模式和原型模式
创建自定义引用类型的默认模式。构造函数模式用于定义实例属性,原型模式用于定义方法共享的属性。
function Person(name, age) { this.name = name this.age = age this.friends = ['ming', 'hong'] } Person.prototype = { constructor: Person, sayName: function () { console.log(this.name) } } const person1 = new Person('Tom', 27) const person2 = new Person('Bob', 30) person1.friends.push('guo') console.log(person1.friends) // ["ming", "hong", "guo"] console.log(person2.friends) // ["ming", "hong"] console.log(person1.friends === person2.friends) // false console.log(person1.sayName === person2.sayName) // true
6.2.5、动态原型模式
解决构造函数与原型分别独立的问题。通过在构造函数中初始化原型,将所有信息封装在构造函数中。
function Person (name, age) { this.name = name this.age = age if (typeof this.sayName !== 'function') { Person.prototype.sayName = function () { console.log(this.name) } } } const person1 = new Person('Tom', 27) person1.sayName() // Tom const person2 = new Person('Bob', 30) person2.sayName() // Bob person1.sayName === person2.sayName // true
上面实例化 person1 的时候,发现 person1 实例化对象上以及其原型上都没有 sayName,于是在 Person 构造函数的原型上增加 sayName 方法。在实例化 person2 的时候,发现 person2 实例化对象上没有 sayName 但是其原型上有。于是就实现了在构造函数内初始化原型。
6.2.6、寄生构造函数模式
利用一个函数来封装创建对象的代码,然后再返回新创建的对象。
function Person (name, age) { var o = new Object() o.name = name o.age = age o.sayName = function () { console.log(this.name) } return o } const person1 = new Person('Tom', 27) const person2 = new Person('Bob', 30) person1.sayName() // Tom person2.sayName() // Bob person1 instanceof Person // false person2 instanceof Person // false person1.sayName === person2.sayName // false
除了使用 new 操作符并且 Person 函数首字母大写之外,这个模式跟工厂模式一模一样。构造函数在不返回值的情况下,默认返回新对象实例。如果末尾有 return 语句,则可以重写调用构造函数时的返回值。需要注意一点:返回的对象与构造函数或构造函数的原型属性之间没有关系,不能依赖 instanceof 操作符来确定对象类型。
假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改 Array 构造函数,因此可以使用该模式。
function SpecialArray () { var values = new Array() values.push.apply(values, arguments) values.toPipedString = function () { return this.join('|') } return values } const colors = new SpecialArray('red', 'blue', 'green') colors.toPipedString() // "red|blue|green"
6.2.7、稳妥构造函数模式
稳妥对象,指的是没有公共属性,而且其方法也不引用 this 对象。与寄生构造函数两点不同:一是新创建对象的实例方法不引用 this,二是不使用 new 操作符调用。与寄生函数相同点是:创建的对象与构造函数之间没有什么关系。
function Person (name, age) { const o = new Object() // 可以在这里定义私有变量和函数 // 添加方法 o.sayName = function () { console.log(name) } return o } const person1 = Person('Tom', 27) person1.sayName() // Tom person1 instanceof Person // false
除了调用 sayName 方法外没有其他方式访问其数据成员。
6.3、继承(原型链)
OO语言三大特点:封装,继承,多态。其中继承是最为津津乐道。OO语言两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于JS函数没有签名,因此无法实现接口继承。只支持实现继承,依靠原型链来实现。
利用原型链,实现自定义类型之间的继承。
6.3.1、原型链
基本原理:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。假如我们让原型对象等于另一个构造函数 Fa 的实例 Oa ,因为实例 Oa 有内部指针指向类型 Fa 的原型 Pa,因此这个原型对象也包含一个内部指针指向 Pa。这就是原型链的概念。本质是重写一个构造函数的原型对象,代之以一个新类型的实例。
function Super () { this.name = 'Tom' } Super.prototype.getSuperName = function () { return this.name } function Sub () { this.subName = 'Bob' } Sub.prototype = new Super() // 继承 Sub.prototype.getSubName = function () { return this.subName } const instance = new Sub() instance.getSubName() // Bob instance.getSuperName() // Tom instance.constructor === Super // true
6.3.1-1 默认原型
所有对象实例,包括每个函数的默认原型 prototype 对象都是 Object 的实例,因此所有默认原型都会包含一个内部指针 __proto__ 指向 Object.prototype,这就是所有自定义类型都会继承 toString(), valueOf() 等默认方法的根本原因。
6.3.1-2 确定原型和实例关系
可以用两种方式来确定原型和实例之间的关系,第一种方式是使用 instanceof 操作符,只要用这个操作符来来测试实例与原型链中出现过的构造函数,结果就会返回 true。
instance instanceof Object instance instanceof Super instance instanceof Sub
第二种是 isPrototypeOf() 方法,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。
Object.prototype.isPrototypeOf(instance)
Super.prototype.isPrototypeOf(instance)
Sub.prototype.isPrototypeOf(instance)
6.3.1-3 定义方法
子类型有时需要覆盖超类中的方法,或添加超类中不存在的方法,注意一定要在替换原型的语句之后;并且不能用对象字面量,因为那会切断原型链。
6.3.1-4 原型链的问题
第一个问题:原型中的引用类型值会被所有实例共享。下面 Sub 的原型是 Super 的实例,其中有引用类型值 color,就会出问题。
function Super () { this.colors = ['red', 'blue', 'green'] } function Sub () {} Sub.prototype = new Super() // 继承 const instance1 = new Sub() instance1.colors.push('black') const instance2 = new Sub() instance2.colors // ['red', 'blue', 'green', 'black']
第二个问题,在创建子类型的时候,不能向超类型的构造函数中出传递参数。
6.3.2、借用构造函数
在子类型构造函数中调用超类型构造函数。通过 apply() 或 call() 方法可以在将来新创建的对象上执行构造函数。这样一来 Sub 的每个实例都会有自己的 colors 副本了。
function Super () { this.colors = ['red', 'blue', 'green'] } function Sub () { Super.call(this) } Sub.prototype = new Super() const instance1 = new Sub() instance1.colors.push('black') const instance2 = new Sub() instance2.colors // ['red', 'blue', 'green']
问题:无法避免构造函数模式存在问题,方法都在构造函数中定义,函数复用就无从谈起了。而且在超类型的原型中定义的方法,对子类型而言也是不可见的。结果所有类型都只能使用构造函数模式。
6.3.3、组合式继承
原型链与借用构造函数的组合式继承,也叫伪经典继承。JavaScript 仿类的经典实现方式。通过在超类型的原型上定义方法实现函数复用,又能利用构造函数保证每个实例拥有自己的属性。
function Super (name) { this.name = name this.colors = ['red', 'blue', 'green'] } Super.prototype.sayName = function () { console.log(this.name) } function Sub (name, age) { Super.call(this, name) this.age = age } Sub.prototype = new Super() Sub.prototype.constructor = Sub Sub.sayAge = function () { console.log(this.age) } const instance1 = new Sub('Tom', 27) instance1.colors.push('black') instance1.colors // ['red', 'blue', 'green', 'black'] instance1.sayName() // Tom instance1.sayAge() // 27 const instance2 = new Sub('Bob', 30) instance2.colors // ['red', 'blue', 'green'] instance2.sayName() // Bob
6.3.4、原型式继承
通过 object 函数,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个实例。从本质上讲,object() 对传入其中的对象执行了一次浅复制。注意它的问题与使用原型模式一样,包含引用类型值的属性始终会共享响应的值。
function object(o) { function F() {} F.prototype = o return new F() } const person = { name: 'Grand', friends: ['red', 'blue', 'green'], sayName: function () { console.log(this.name) } } const person1 = object(person) person1.name = 'Tom' person1.sayName() // Tom person1.friends.push('black') const person2 = object(person) person2.friends // ["red", "blue", "green", "black"]
ECMAScript 5 通过新增 Object.create() 规范化了原型式继承。它接收两个参数,一个是用作新对象原型的对象,一个是为新对象定义额外属性的对象。
6.3.5、寄生式继承
function object(o) { function F() {} F.prototype = o return new F() } function createAnother(original) { const clone = object(original) clone.sayHi = function () { console.log('hi') } return clone }
与构造函数的问题一样,不能做到函数复用。
6.3.6、寄生组合式继承