# 面向对象
# 数据类型的分类和判断
基本数据类型:
- Number(Infinity、NaN 、任意数值) ----- typeof
- Boolean (true、false)----- typeof
- String 任意字符串 ----- typeof
- Null --------- ===
- undefined --- typeof/===
- 特点:存储在栈中,一个值对应一个变量
引用类型值:
- Object ----- typeof/instanceof
- Array ------ instanceof
- Function ---- typeof
- 特点:所有引用类型值都是对象,所有对象都是function;存储在堆,一个变量对应一个指向堆地址的指针
instanceof 用来判断对象的类型
# 对象的理解和使用
什么是对象:
ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”
对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。即 ECMAScript 的对象是一组键值对,其中值可以是数据或函数。
属性组成:
- 属性名 : 字符串(标识)
- 属性值 : 任意类型
属性的分类:
- 一般 : 属性值不是function 描述对象的状态
- 方法 : 属性值为function 描述对象的行为
特别的对象:
- 数组: 属性名是0,1,2,3之类的索引
- 函数: 可以执行的
- JSON: 对象的字符串表示,用来传输数据
如何操作内部属性:
- .属性名
- [ '属性名' ] : 属性名有特殊字符/属性名是一个变量
查看某属性是否在对象中:
对象的一个显著的特点就是可以访问任何属性,如果这个属性名没有值也不会有错误,访问一个不存在的属性会返回 undefined。它提供一种普遍的方法去检查属性是否存在 —— 获得值来与 undefined 比较:
let user = {};
alert( user.noSuchProperty === undefined ); // true 意思是没有这个属性
一般情况检验属性值是否 == undefined即可,但是也存在属性值就是undefined情况
使用操作符 "in"
来检查是否属性存在**
语法:"key" in object
;需要注意 in
的左边必须是属性名。通常是一个字符串,如果不用字符串,那就是一个字符串变量。
let user = { age: 30 };
let key = "age";
alert( key in user ); // true,获取键的名字和检查这个键的属性
删除对象的属性:
delete 用于删除对象的属性
用法:delete Object.attribute 删除对象的属性
# 克隆对象
// 声明一个对象
var user = {
name: "李白",
age: 29,
hobbies: ["唱歌","吟诗","旅游"],
sayName: function (){
alert(this.name);
}
}
简单克隆(浅克隆 )
// 创建一个新的对象,简单类型值不会改变,引用对象值会跟着改变
function clone(obj){
var newObj = {};
for(var i in obj){
newObj[i] = obj[i];
}
return newObj;
}
let obj1 = user; // 指向同一个地址,一个改变,另一个跟着改变
let obj1 = { ...user } // 创建一个新的对象,简单类型值不会改变,引用对象值会跟着改变
let obj1 = JSON.parse(JOSN.stringigy(user)) // 无法拷贝函数
/* 使用Object.assign 来拷贝对象
Object.assign(dest[, src1, src2, src3...])
参数 dest 和 src1, ..., srcN(可以有很多个)是对象。
这个方法复制了 src1, ..., srcN 的所有对象到 dest。换句话说,从第二个参数开始,所有对象的属性都复制给了第一个参数对象,然后返回 dest
如果接收的对象已经有了同样属性名的属性,前面的会被覆盖
*/
let obj1 = Object.assign({},user); // 创建一个新的对象,简单类型值不会改变,引用对象值会跟着改变
深度克隆
// 为了解决上面的的问题,我们在复制的时候应该检查 `user[key]` 的每一个值,如果是一个对象,我们再复制一遍这个对象,可以利用 递归思想
function deepClone(obj){
if(obj === null) return null;
if(typeof obj !== 'object') return obj;
if(obj instanceof RegExp) return new RegExp(obj);
if(obj instanceof Date) return new Date(obj);
let newObj = {};
for(let key in obj){
newObj[key] = deepClone(obj[key]);
}
return newObj;
}
let user1 = deepClone(user);
user.hobbies.push("drink");
console.log(user1.hobbies);
# 函数的理解和使用
什么是函数:
用来实现特定功能的, n条语句的封装体
使用函数有什么好处?
- 提高复用性
- 便于阅读交流
立即执行函数:
// 1. 函数表达式的立即执行
var fn = function (){
console.log("函数表达式的立即执行");
}();
// 2. 声明函数的立即执行
(function fn(){
console.log("立即执行");
}())
(function fn(){
console.log("立即执行");
})()
// 3. 函数表达式使用两个括号时
var fn = (function(){
console.log("函数表达式的立即执行");
}())
var fn = (function(){
console.log("函数表达式的立即执行");
})()
递归函数:
// 找规律 n! = n * (n-1)!
function jc(n){
if(n === 1){
return 1;
}
return n * jc(n-1);
}
函数的参数:
简单类型值作为形参
引用类型值作为形参
arguments 的使用
function add(a,b){ console.log(a,arguments[0],a === arguments[0]); // 1 1 true a = 4; console.log(arguments[0]); // 4 arguments[0] = 6; console.log(a); // 6 } add(1,2); // 函数的形参就是 arguments 数组中对应位置的值 比如 a === arguments[0] ====> true // 形参的变化反映在arguments中,类似镜子内外的映射 // arguments.callee 相当于函数本身
回调函数:
常用场景: 定时器的回调函数/ajax的回调函数/其它需要异步处理数据的情景
setTimeout(function (){
// 模拟请求 3s后拿到数据
var a = 10;
// 使用A页面的回调函数处理数据
fn(a);
// 使用B页面的回调函数处理数据
gn(a);
},3000)
// A.html
function fn(n){
console.log('处理3s后的数据a==>', n+1);
}
// B.html
function gn(n){
console.log('处理3s后的数据a==>', n+1)
}
# 上下文对象创建和初始化
- 全局:
- 在全局代码执行前最先创建一个全局上下文对象(window)
- 收集一些全局变量/函数, 并初始化
- 将这些变量设置为上下文对象的属性
- 函数:
- 在调用函数时, 在执行函数体之前先创建一个函数上下文对象
- 收集一些局部变量,/函数 并初始化
- 将这些变量设置为上下文对象的属性
- 声明提升
- 变量提升: 在变量定义语句之前, 就可以访问到这个变量(undefined)
- 函数提升: 在函数定义语句之前, 就执行该函数
- 如果一个变量和函数同名,函数声明优先于变量声明
练习:
var foo = 1;
function bar() {
if (!foo) {
var foo = 10;
}
alert(foo); // ?
}
bar();
var a = 1;
function b() {
a = 10;
return;
function a() {}
}
b();
alert(a); // ?
严格模式:
‘use strict’
为了规范 js 的语言类型,推出了严格模式,严格模式下未经声明的值不可使用。
# 闭包
理解:
- MDN 上面这么说**:**闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
- 当嵌套的内部函数引用了外部函数的变量时就产生了闭包
- 闭包的本质就是:指有权访问另一个函数作用域中的变量的函数
作用:
- 延长局部变量的生命周期
- 让函数外部能操作内部的局部变量
写一个闭包
function fn1() { var a = 2; function fn2() { a++; console.log(a); } return fn2; } var f = fn1(); f(); f();
闭包的作用域链包含着它自己的作用域,以及包含它的函数的作用域和全局作用域
闭包应用:
- 回调函数
- 模块化编码: 封装一些数据以及操作数据的函数, 向外暴露一些行为
- 循环遍历加监听
缺点:
变量占用内存的时间可能会过长
可能导致内存泄露
解决:
及时释放 : f = null; // 让内部函数对象成为垃圾对象
内存泄露与内存溢出
- 内存泄露
- 定义: 不再需要的内存对象, 因为某种原因不能成为垃圾对象而一直不能被回收(占用内存)
- 例如: 意外的全局变量, 定时器, 闭包...
- 问题: 导致可用内存减小
- 解决: 及时释放内存
- 内存溢出
- 定义: 申请/需要的空间超过了剩余的最大空间, 程序就会抛出'内存溢出的错误
- 例如: 不断创建对象/数组(大)
- 问题: 运行出错, 程序停止
- 解决: 减小申请内存, 及时释放内存
- 关系
- 内存泄露如果不断累积, 就有可能导致内存溢出的错误
闭包的题
function makeCount(){
let count = 0;
return function (){
return count ++;
}
}
let count1 = makeCount();
let count2 = makeCount();
console.log(count1()); //
console.log(count1()); //
console.log(count2()); //
function fun(n,o){
console.log(o);
return {
fun: function(m){
return fun(m,n);
}
};
}
var a = fun(0); // ?
a.fun(1); // ?
a.fun(2); // ?
a.fun(3); // ?
var b = fun(0).fun(1).fun(2).fun(3); // ?
var c = fun(0).fun(1); // ?
c.fun(2); // ?
c.fun(3); // ?
作业:
编写一个像 sum(a)(b) = a+b 这样工作的 sum 函数。
// eg: sum(1)(2) = 3; sum(5)(-1) = 4 function sum(a){ return function (b){ return a + b; } }
简化 sort 按字段排序
// 有一组需要排序的对象 let users = [ { name: "John", age: 20, surname: "Johnson" }, { name: "Pete", age: 18, surname: "Peterson" }, { name: "Ann", age: 19, surname: "Hathaway" } ]; // 通常的做法: // 通过 name (Ann, John, Pete) users.sort((a, b) => a.name > b.name ? 1 : -1); // 通过 age (Pete, Ann, John) users.sort((a, b) => a.age > b.age ? 1 : -1); // 要求简化为: users.sort(byField('name')); users.sort(byField('age')); [].sort(function (key){ return function (a,b){ if( a[key] > b[key]){ return 1; }else if(a[key] < b[key]){ return -1 }else { return 0 } } }) function byField(key){ return (a,b) => { return a[key] - b[key]; } }
# 描述对象属性的特性的属性
到目前为止,属性对我们来说是一个简单的“键-值”对。但对象属性实际上是更复杂可变的东西。
两种描述属性:数据属性和访问器属性
# 1. 数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性
[[Configurable]] :是否可配置标志,表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
有时它会预设在内置对象和属性中。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true
当它的值为false的时候,一个不可配置的属性不能被
defineProperty
删除或修改。[[Enumerable]] :表示能否通过 for-in 循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true
[[Writable]] :表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true
[[Value]] :包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined
像上面的两种方法那样直接在对象上定义属性和方法,它们的 [[Configurable]] 、 [[Enumerable]] 和 [[Writable]] 特性都被设置为 true ,而 [[Value]] 特性被设置为指定的值。可以通过 Object.defineProperty() 来定义
属性描述符可以在各个属性的级别上工作。还有一些限制访问整个对象的方法:
-
禁止向对象添加属性。
-
禁止添加/删除属性,为所有现有的属性设置
configurable: false
。 -
禁止添加/删除/更改属性,为所有现有属性设置
configurable: false, writable: false
。
还有对他们的测试:
-
如果添加属性被禁止,则返回
false
,否则返回true
。 -
如果禁止添加/删除属性,则返回
true
,并且所有现有属性都具有configurable: false
。 -
如果禁止添加/删除/更改属性,并且所有当前属性都是
configurable: false, writable: false
,则返回true
。
这些方法在实践中很少使用。
# 2. 访问器属性
访问器属性不包含数据值;它包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter 函数并传入新值,这个函数负责决定如何处理数据。
- [[Get]] :在读取属性时调用的函数。默认值为 undefined
- [[Set]] :在写入属性时调用的函数。默认值为 undefined
在对象字面量中,它们用 get
和 set
表示:
// 从外表看,访问器属性看起来像一个普通的属性。这是访问器属性的设计思想。我们不以函数的方式调用 user.fullName,我们通常读取它:getter 在幕后运行。
// 这样就创建了一个“虚拟”属性。它是可读写的,但实际上并不存在。
let obj = {
get propName() {
// getter, the code executed on getting obj.propName
},
set propName(value) {
// setter, the code executed on setting obj.propName = value
}
};
# 访问器属性的描述符
访问器属性的描述符与数据属性相比是不同的。
对于访问器属性,没有 value
和 writable
,但是有 get
和 set
函数。
所以访问器描述符有:
- get —— 一个没有参数的函数,在读取属性时工作,
- set —— 带有一个参数的函数,当属性被设置时调用,
- enumerable —— 与数据属性相同,
- configurable —— 与数据属性相同。
例如,要使用 defineProperty
创建 fullName
的访问器,我们可以使用 get
和 set
来传递描述符:
let user = {
name: "John",
surname: "Smith"
};
Object.defineProperty(user, 'fullName', {
get() {
return `${this.name} ${this.surname}`;
},
set(value) {
[this.name, this.surname] = value.split(" ");
}
});
alert(user.fullName); // John Smith
for(let key in user) alert(key); // name, surname
注意: 属性可以是“数据属性”或“访问器属性”,但不能同时属于两者。
// 同时存在 get函数和 value,会报错
Object.defineProperty({}, 'prop', {
get() {
return 1
},
value: 2
});
# 3. defineProperty
要修改属性默认的特性,必须使用 Object.defineProperty()
方法。
语法:
Object.defineProperty(obj, propertyName, descriptor)
// obj,propertyName 要处理的对象和属性。
// descriptor 要应用的属性描述符。
例如:
var person = {};
Object.defineProperty(person, "name", {
enumerable: true, // 是否可枚举,默认值为 true; 如果为false,打印这个对象,是看不到name这个属性的
writable:true, // 是否可写,默认值 true; 如果为false的话,给name赋值,不会生效
configurable:true, // 是否可配置(是否可删除),默认值 true
// 如果为true,delete obj.name,再打印person,则显示{}
// 如果为false,delete obj.name,再打印person,则显示{name:Nicholas}
value:'Nicholas', // name对应的值
});
alert(person.name);
delete person.name;
alert(person.name);
Object.defineProperty(person,"name",{
get: function (){
// 当读取person.name时回调用此函数,拿取返回值
return 'xxx';
},
set: function (newValue){
// 更改person.name的值时,回调用此函数,默认形参为我们设置的新值
person.name = newValue;
}
})
注意:
使属性不可配置是一条单行道。我们不能把它改回去,因为
defineProperty
不适用于不可配置的属性。设置不可用的属性时,只在使用严格模式时才会出现错误。在非严格模式下,写入只读属性等时不会发生错误。但操作仍然不会成功。非严格模式下违反标志的行为只是默默地被忽略。
# 双向绑定
<input type="text" v-module="name">
<script type="text/javascript">
var obj = {};
var ipt = document.querySelector("input[v-module]");
ipt.oninput = () => {
obj.name = ipt.value;
}
Object.defineProperty(obj,'name',{
get(){
return ipt.value
},
set(v){
ipt.value = v;
}
})
</script>
# 4. defineProperties
Object.defineProperties() 方法用来一次定义多个属性,这个方法接受两个对象参数: 添加或修改其属性的对象,与第一个对象中要添加或修改的属性一一对应的对象
语法:
Object.defineProperties(obj, {
prop1: descriptor1,
prop2: descriptor2
// ...
});
例如:
Object.defineProperties(user, {
name: { value: "John", writable: false },
surname: { value: "Smith", writable: false },
// ...
});
# 5. getOwnPropertyNames
Object.getOwnPropertyNames(obj) 可以得到 obj 所有的属性,无论它是否可枚举。
Object.getOwnPropertyNames(person1) // ["name", "age", "job", "sayName"]
# 原型与原型链
前面讲过,在 JavaScript 里,函数即对象,程序可以随意操控它们。比如,可以把函数赋值给变量,或者作为参数传递给其他函数,也可以给它们设置属性,甚至调用它们的方法。下面示例代码对「普通对象」和「函数对象」进行了区分。
普通对象:
var o1 = {};
var o2 = new Object();
函数对象:
function f1(){};
var f2 = function(){};
var f3 = new Function('str','console.log(str)');
# 一、Prototype
无论什么时候,只要创建了一个新函数,js 就会根据一组特性的规则为该函数创建一个 prototype (原型)属性,这个属性是一个指针,指向函数的原型对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
凡是使用 function
关键字或 Function
构造函数创建的对象都是函数对象。而且,只有函数对象才拥有 prototype
(原型)属性。
在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。也就是说该构造函数指向我们创建的构造函数本身。 创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,都是从 Object 继承而来的。
# 二、constructor
函数还有一种用法,就是把它作为构造函数使用。像 Object
和 Array
这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而自定义对象类型的属性和方法。如下代码所示:
function Person(name, age){
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name);
};
}
var person1 = new Person("Stone", 28);
var person2 = new Person("Sophie", 29);
你应该注意到函数名 Person
使用的是大写字母 P
。按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法借鉴自其他面向对象语言,主要是为了区别于 JavaScript 中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。
# 三、__proto__
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262 第 5 版中管这个指针叫 [[Prototype]] 。虽然在脚本中没有标准的方式访问 [[Prototype]] ,但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性__proto__
;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
# 四、Prototype与__proto__
的关系
函数的prototype: 定义函数时被自动赋值, 值默认为{}, 即用为原型对象
实例对象的
__proto__
: 在创建实例对象时被自动添加, 并赋值为构造函数的prototype值构造函数、原型对象、实例之间的关系如下图:
# 五、关于原型对象的方法
# hasOwnProperty
// hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。只在给定属性存在于对象实例中时,才会返回 true 。
function Person(){
this.name = '楚留香'
}
Person.prototype.age = 24;
let p = new Person()
console.log(p.hasOwnProperty('name')); // true
console.log(p.hasOwnProperty('age')); // false
# isPrototypeOf
// isPrototypeOf 判断一个对象是否是某个实例的原型对象
function Person(){
this.name = '威震天'
}
let p = new Person()
let obj = Person.prototype
obj.isPrototypeOf(p) // true
# getPrototypeOf
// getPrototypeOf 获取一个实例的原型对象
function Person(){
this.name = "威震天"
}
let we = new Person();
let obj = Object.getPrototypeOf(we);
console.log(obj);
console.log(obj == Person.prototype)
# Object.keys
// 获取所有对象可枚举的key
function User(name){
this.name = name;
}
User.prototype.sex = 0;
let user = new User('威震天');
console.log(Object.keys(user1)); // ["name", "sex"]
# 创建对象
# 1. 原始模式创建对象
实例化Object方法
var user = new Object(); user.name = "Jhon"; user.age = 20; user.say = function (){ console.log(this.name); }
字面量形式
var user{ name: "Jhon", age: 20, say: function (){ console.log(this.name); } }
衍生问题:
虽然 Object 构造函数或对象字面量都可以用来创建对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用其他方式批量创建对象
# 2. 原始模式的改进
写一个函数,然后生成实例对象,从而快速的批量创建对象。这种方式称为“工厂模式”。
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function (){
console.log(this.name);
}
return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
在上例中:函数 createPerson() 能够根据接受的参数来构建一个包含所有必要信息的 Person 对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
// 例如:我们声明一个创建 dog 的函数
function createDog(name,age,skill){
var o = new Object();
o.name = name;
o.age = age;
o.skill = skill;
return o;
}
var dog = createDog("旺财",10,"lookHouse");
console.log(dog.constructor == person1.constructor); // true
console.log(dog.__proto__ == person1.__proto__); // true
// 这样我们没有办法区分创建的对象是人还是狗
# 3. 构造函数模式
所谓"构造函数"(Constructor),其实就是一个普通函数,但是内部使用了this
变量。对构造函数使用new
运算符,就能生成实例,并且this
变量会绑定在实例对象上。
function Person(name, age, job){
// 添加实例的属性和方法,new 出来的实例都具有这些属性和方法
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
// 绑定在构造函数的上的属性和方法,需要通过构造函数访问
if (!Person.total) {
Person.total = 0
}
Person.total++;
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
/* new 操作符的作用
(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
(3) 执行构造函数中的代码(为这个新对象添加属性);
(4) 返回新对象。
*/
跟工厂模式对比:
没有显式地创建对象
直接将属性和方法赋给了 this 对象
没有 return 语句,默认返回 this
应该注意函数名 Person 使用的是大写字母 P。按照惯例,构造函数的名字始终都应该以一个大写字母开头
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方
function Person(name){ this.name = name; } function Dog(name){ this.name = name; } var person = new Person() var dog = new Dog() console.log(person.constructor == dog.constructor); // false
存在的问题:
构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中, person1 和 person2 都有一个名为 sayName() 的方法,但那两个方法不是同一个 Function 的实例。
# 4. 原型模式
prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
function Person(){};
// 添加在构造函数的原型上的属性和方法,会被 new 出来的实例共享,但是又不会被实例的更改所影响
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
上例已经通过原型方法增加了复用性,但是,如果我要给原型对象添加大量的属性方法时,我们不断的使用Person.prototype,xxx=xxx,这样也很繁琐。为了简化流程可以让 Person.prototype 指向一个新的对象,例如
function Person(){};
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer";
sayName: function (){
alert(this.name);
}
}
注意:这样会更改原型对象的constructor指针
原型对象默认是有一个指针 constructor 指向其构造函数的,如果我们把构造函数的原型对象,替换成另外一个对象,这个对象的 constructor 就不再是指向该构造函数,而是指向新对象的 constructor。
console.log(Person.prototype.constructor); // Object
// 解决这个问题,可以把原型对象的constructor指向到Person
Person.prototype.constructor = Person
console.log(Person.prototype.constructor) // Person
# 原型模式的其它用法
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型( Object 、 Array 、 String ,等等)都在其构造函数的原型上定义了方法。
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。
String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
console.log(msg.startsWith("Hello")); // true
注意:尽管可以这样做,但我们不推荐在产品化的程序中修改原生对象的原型。如果因某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支持该方法的实现中运行代码时,就可能会导致命名冲突。而且,这样做也可能会意外地重写原生方法。
# 原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来,即使是先创建了实例后修改原型也照样如此。请看下面的例子。
var friend = new Person();
Person.prototype.sayHi = function(){
console.log("hi");
};
friend.sayHi(); // "hi"(没有问题!)
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。
上面提到:调用构造函数时会为实例添加一个指向最初原型的 [[Prototype]]
指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。看下面的例子。
function Person(){}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Stone",
age : 28,
sayName : function () {
console.log(this.name);
}
};
friend.sayName(); // Uncaught TypeError: friend.sayName is not a function
在这个例子中,我们先创建了 Person
的一个实例,然后又重写了其原型对象。然后在调用 friend.sayName()
时发生了错误,因为 friend
指向的是重写前的原型对象,其中并不包含以该名字命名的属性。
# 原型对象的问题
- 无法传递初始化参数
- 共享的本性导致对于包含的引用类型值的属性会出问题
function Person(){}
Person.prototype = {
constructor: Person,
name: "zhangsan",
frinds: ["lisi","wangwu"],
sayName: function (){
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.frinds.push("zhaoliu");
console.log(person2.frinds); // ['lisi','wangwu','zhaoliu']
console.log(person2.frinds == person1.frinds); // true
# 5. 组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
function Person(name){
this.name = name;
this.hobbies = ['sing','dance'];
}
Person.prototype = {
sayName: function(){
alert(this.name);
}
}
var jhon = new Person("Jhon");
var jina = new Person("Jina");
jhon.hobbies.push("run");
console.log(jhon.hobbies); // ["sing", "dance", "run"]
console.log(jina.hobbies); // ["sing", "dance"]
console.log(jhon.say == jina.say); // true
好处:每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。
# 6. 动态原型模式
有其他语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。
function Person(name){
this.name = name;
this.hobbies = ['sing','dance'];
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var libai = new Person("libai");
libai.sayName();
不必用一大堆if 语句检查每个属性和每个方法;只要检查其中一个即可。
还可以使用 instanceof 操作符确定它的类型。
注意:使用动态原型模式时,不能使用对象字面量重写原型。
# 对象继承
# Call 和 apply
# 1. call 方法
语法:func.call(thisObj,arg1,arg2,arg3...)
定义:调用一个对象的一个方法,以另一个对象替换当前对象
说明:它运行一个方法,提供的第一个参数作为 this
,后面的作为参数。如果没有提供 thisObj 参数,那么 Global 对象被用作 thisObj
例如:
var name = "张三";
var obj = {
name: "李白"
}
function sayName(){
return this.name;
}
sayName(); // 张三
sayName.call(obj); // 李白
function sing(lyric){
return this.name + '在唱:'+ lyric;
}
sing("狼烟起江山北望");
sing.call(obj,"我在遥望,月亮之上")
# 2. apply 方法
语法:func.apply(thisObj,[argArray])
定义:应用某一对象的一个方法,以另一个对象替换当前对象,第二个参数是数组或类数组
说明:如果第二个参数不是数组或类数组,那么将导致一个 typeError。如果没有提供 thisObj,那么 Global 对象将被用作 thisObj,并且无法被传递任何参数。
var name = "张三";
var obj = { name : "李白" };
function sayFriend(f1,f2){
return this.name + '的朋友有:' + f1 + '、' + f2;
}
sayFriend("李四","王五");
sayFriend.call(obj,"杜甫","白居易");
sayFriend.apply(obj,['杜甫',"白居易"]);
call
和 apply
之间唯一的语法区别是 call
接受一个参数列表,而 apply
则接受带有一个类似数组的对象。
但是当前可以使用 spread 运算符将数组作为参数列表传递,使用 call 模拟 apply
sayFriend.call(obj,...['杜甫',"白居易"])
// 但是这种使用方式没有 apply 解析速度快
apply
最重要的用途之一是将调用传递给另一个函数,如下所示:
let wrapper = function() {
return anotherFunction.apply(this, arguments);
};
这叫做 呼叫转移。wrapper
传递它获得的所有内容:上下文 this
和 anotherFunction
的参数并返回其结果。
新的检验类型的方法:
var s = Object.prototype.toString;
s.call(123); // "[object Number]"
s.call('dsf') // "[object String]"
s.call({}) // "[object Object]"
s.call([]) // "[object Array]"
s.call(true) // "[object Boolean]"
s.call(undefined)// "[object Undefined]"
s.call(null) // "[object Null]"
s.call(NaN) // "[object Number]"
s.call(Infinity)// "[object Number]"
s.call(new Date())// "[object Date]"
s.call(Math) // "[object Math]"
s.call(JSON) // "[object JSON]"
# 对象继承
继承定义:继承可以使子类具有父类的各种属性和方法,而不需要再次编写相同的代码
面向对象的继承方式有很多种,原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生式组合继承、深拷贝继承等等。
# 原型链继承
利用原型链的特性,当在自身找不到时,会沿着原型链往上找。
function Person(){
this.name = '李白'
this.hobbies = ['吟诗','旅游']
}
Person.prototype.sayName = function(){
console.log(this.name)
}
function Student(){
this.age = "24"
}
let student = new Student()
console.log(student.age) // '24'
console.log(student.name) // undefined
student.sayName() // 报错
如果想让 student 访问到 Person的原型的对象的属性和方法,可以将 Student.prototype 改写为 Person 的实例
// 改写Student.prototype指针指向
Student.prototype = new Person();
这就是原型链继承
原型链的问题:
原型对象类似一个共享库,所有实例共享原型对象同一个属性方法,如果原型对象上有引用类型,那么会被所有实例共享,也就是某个实例更改了,则会影响其他实例,例如:
var student2 = new Student()
// 此时我们修改某一个实例,hobbies是原型对象上的引用类型 数组
student.hobbies.push('喝酒')
console.log(student.hobbies) // ['吟诗', '旅游', '喝酒']
console.log(student2.hobbies) // ['吟诗', '旅游', '喝酒']
从上面可以看出,student 的 hobbies(实际就是原型对象上的 hobbies)被修改后,相关的实例student2也会受到影响。
解决:把Person上的属性方法添加到Student上,以防都存在原型对象上,会被所有实例共享,特别是引用类型的修改,会影响所有相关实例。
# 借用构造函数继承
有时候也叫做伪造对象或经典继承,可以利用call来实现。
function Person(){
this.name = '李白'
this.hobbies = ['吟诗','旅游']
}
Person.prototype.sayName = function(){
console.log(this.name)
}
function Student(){
Person.call(this) // 利用call调用Person上的属性方法拷贝一份到Student
this.age = 24
}
Student.prototype = new Person()
let student = new Student()
let student2 = new Student()
console.log(student.age) // 24
console.log(student.name) // '李白'
console.log(student.hobbies) // '['吟诗','旅游']'
student.sayName() // '李白'
// 此时我们修改某一个实例,hobbies 是原型对象上的引用类型 数组
student.hobbies.push('喝酒')
console.log(student.pets) // ['吟诗', '旅游', '喝酒']
console.log(student2.pets) // ['吟诗', '旅游']
上面在子构造函数(Student)中利用call调用父构造函数(Person)的方式,叫做借助构造函数继承
结合上面所看,使用了原型链继承和借助构造函数继承,两者结合起来使用叫组合继承,关系图如下:

那么还有个问题,当父构造函数需要接收参数时,怎么处理?
function Person(name,hobbies){ // 父构造函数接收name,hobbies参数
this.name = name // 赋值到this上
this.hobbies = hobbies // 赋值到this上
}
Person.prototype.sayName = function(){
console.log(this.name)
}
function Student(age,name,hobbies){ // 在子构造函数中也接收参数
Person.call(this,name,hobbies) // 在这里把name和hobbies传参数
this.age = age // 赋值到this上
}
Student.prototype = new Person()
Student.prototype.constructor = Student
let student = new Student(24,"李白",["吟诗","旅游"])
let student2 = new Student(22,"杜甫",["忧虑","想李白"])
console.log(student.age) // 24
console.log(student.name) // '李白'
console.log(student.pets) // '["吟诗","旅游"]'
student.sayName() // '李白'
student.hobbies.push('喝酒')
console.log(student.hobbies) // '["吟诗","旅游","喝酒"]'
console.log(student2.hobbies) // '["吟诗","旅游"]'
这样我们就可以在子构造函数中给父构造函数传参了,但是使用Person.call(this,name,hobbies)
和new Person()
实例中的属性重复了,能否在子构造函数设置原型对象的时候,只要父构造函数的原型对象属性方法呢?当然是可以的,接下来的寄生式组合继承,也是目前业界认为解决继承问题最好的方案
# 寄生式组合继承
function Person(name,hobbies){ // 父构造函数接收name,hobbies参数
this.name = name // 赋值到this上
this.hobbies = hobbies // 赋值到this上
}
Person.prototype.sayName = function(){
console.log(this.name)
}
function Student(age,name,hobbies){ // 在子构造函数中也接收参数
Person.call(this,name,hobbies) // 在这里把name和hobbies传参数
this.age = age // 赋值到this上
}
// 寄生式继承
function Temp(){} // 声明一个空的构造函数,用于桥梁作用
Temp.prototype = Person.prototype // 把Temp构造函数的原型对象指向Person的原型对象
let temp = new Temp() // 用构造函数Temp实例化一个实例temp
Student.prototype = temp // 把子构造函数的原型对象指向temp
temp.constructor = Student // 把temp的constructor指向Student
let student1 = new Student(24,'李白',['吟诗','旅游'])
console.log(student1) // Student { name: '李白', hobbies: [ '吟诗', '旅游' ], age: '24' }
let student2 = new Student(22,'杜甫',['忧虑'])
console.log(student2) // Student { name: '杜甫',hobbies: [ '忧虑' ], age: 22 }
至此为止,我们就完成了寄生式组合继承了,主要逻辑就是用一个空的构造函数,来当做桥梁,并且把其原型对象指向父构造函数的原型对象,并且实例化一个temp,temp会沿着这个原型链,去找到父构造函数的原型对象
# 原型式继承
// 原型式继承
function createObjWithObj(obj){ // 传入一个原型对象
function Temp(){}
Temp.prototype = obj
let o = new Temp()
return o
}
// 把Person的原型对象当做temp的原型对象
let temp = createObjWithObj(Person.prototype)
// 也可以使用Object.create实现
// 把Person的原型对象当做temp2的原型对象
let temp2 = Object.create(Person.prototype)
# 寄生式继承
// 寄生式继承
// 我们在原型式的基础上,希望给这个对象新增一些属性方法
// 那么我们在原型式的基础上扩展
function createNewObjWithObj(obj) {
let o = createObjWithObj(obj)
o.name = "李白"
o.age = 28
return o
}
# ES6 的class 和 extend
class Animal{
// 构造函数
constructor(color){
this.color = color;
}
move(){
console.log("moving ...");
}
}
class Dog extends Animal{
constructor(name,color){
// 继承父类属性
super(color);
this.name = name;
}
run(){
console.log("running ...");
}
}
var dog = new Dog("旺财","yellow");
dog.move();
console.log(dog.color);
继承暂时用不到,等用到也是1年半载之后了,所以不用可以刻意背,慢慢理解就行了。因为当前的框架都没有明显体验出继承的思想。只有以后有当架构师的,可能会用到
← async await 正则表达式 →