1 -- 面向对象
对象
任何事物都可以看作是对象。
如何使用js语言描述对象
字面量形式创建对象
var p = {};
p.name = '中国人';
p.age = '500';
构造函数形式创建对象
function Person(name, age) {
this.name = name;
this.age = age;
}
var p = new Person('中国人', 500);
var p2 = new Person('中国人2', 500);
面向对象与面向过程的区别
- 面向过程
- 凡是自己亲力亲为,按部就班的解决现有问题。
- 面向对象
- 自己充当一个指挥者的角色,指挥更加专业的对象帮我解决问题。
面向过程的优缺点
- 缺点
- 代码可读性比较差
- 可维护性和可扩展性比较差
- 全局变量污染严重,变量管理混乱
- 优点
- 书写快速,因为通常是想到什么写什么,只要能解决当前问题即可
- 通常来说比面向对象要节省内存
面向对象的优缺点
- 缺点
- 比面向过程要消耗一些内存
- 开发会比过程要慢一些(但也不是绝对的,因为面向对象书写的代码复用性比较强,也就是第一天比较慢,日后可能就比较快了)
- 优点
- 代码可读性比较高
- 可维护性和可扩展性比较高
- 变量的管理比较清晰
面向对象代码的书写
- 要根据需求想象我们需要那些对象帮我解决这个需求(比如我要逛街,需要一个导购,需要一个保镖,需要一个女朋友)
- 编写对象对应的构造函数(比如function Person(){})
- 抽取该对象所需的属性,编写到构造函数内(比如this.name = name)
- 抽取该对象所需的方法,编写到构造函数原型中(比如Person.prototype.run = function(){})
- 把类写完之后,就可以创建实例,解决实际需求了
面向对象的3大特征
- 封装性
- js中的对象本质上就是键值对的集合,是一个复杂数据类型,可以包含N多其他数据,这就是js中对象的封装性。
- 继承性
- js中可以通过某种方式,让一个对象无条件访问另一个对象的属性与方法,这就是继承性。
- js中的所有对象,都具有一个原型对象,可以无条件的访问这个原型对象的属性与方法,这就是js中对象的继承性。
- 多态性
- 在js中,对象可以随时变化,对象继承的对象也可以被随意改变,这些动态的变化可以理解为是js的多态。
类和实例
如果把类看作是模子,实例可以看作是模子刻出来的东西。
- 类就是对某一些具有相同特征与特性的抽象的描述
- 实例相对比类,就是一个实实在在具体的事物。
什么是伪数组?
-
伪数组是一个非数组类型对象
-
有一个length属性,值为number类型
-
存在下标方式存储数据
var obj = { 0:1, 1:'2sfd', length:2 } var obj2 = { length:0 } var obj3 = { 0:1, 1:'2sfd', name:'abc', length:3 }
2 -- 继承
原型
- 原型本身是一个对象,这个对象的属性与方法可供其他对象。
- 任何对象都有成为原型的潜质,下面的代码就让obj成为了原型。
谁有原型
- 默认所有的对象都有原型
谁有prototype
- 默认所有的函数都有prototype
给对象手动添加prototype可以实现继承吗
- 没什么乱用,因为对象不能配合new关键字创建实例。
函数的特殊性
- 函数也是对象的一种,所以也有__proto__
- 函数可以用来创建实例,又有prototype
如何访问一个对象的原型
- 通过__proto__属性(但是它是非标准属性,不建议开发中使用)
- 通过constructor属性得到对象的构造函数,再访问其prototype得到原型
prototype的作用
- 用来引导构造函数创建的实例默认原型
__proto__的作用
- 用来继承某个对象
prototype与__proto__联系
- 通过构造函数创建的实例,实例的__proto__默认为构造函数的prototype,除此之外,没有任何联系。
构造函数创建对象的4个步骤
- 创建一个新对象(本质上就是开辟了一块内存空间)
- 设置新对象的原型
- 本质上就是在这块内存空间中添加了一个__prot__属性
- __proto__属性值与构造函数的prototype有关
- 相当于是这样给__proto__赋值的:新对象.proto = 构造函数.prototype
- 执行构造函数,执行时设置其this为新实例
- 返回新实例的地址
对象的属性访问规则
优先从自身查找,找不到就去原型找,还找不到继续去原型的原型找, 直到终点,终点也没有返回undefined。
对象的属性赋值
自己没有该属性相当于新增,有则修改,并不会对其原型上的属性造成影响。
继承
- 在js中,只要一个对象能够使用另一个对象的成员,这种特征就是继承。
- 在主流的面向对象语言中,继承是类与类之间的关系,在js中继承是对象与对象之间的关系。
继承方式
1、默认的原型继承
function P() {}
P.prototype.fun = function(){};
var p = new P();
2、原型替换
function P() {}
P.prototype = {
constructor: P,
fun: function(){}
};
var p = new P();
3、Object.create
var proObj = {
fun: function(){}
};
var p = Object.create(proObj);
4、原型组合式
function A() {}
function P() {}
P.prototype = Object.create(A.prototype);
P.prototype = new A();
var p = new P();
属性复制
在日常开发中,可能会存在实现多继承的需求,上面的原型组合式就可以完成这个需求。 但是原型组合式如何嵌套过多,对于属性的查找效率是有影响的,而且过长的原型,也不利于维护。 对于实现多继承,还有另外一种解决方案,这种解决方案有一个代表,就是jQuery库中提供的extend方法。
实现属性复制函数封装
function copy() {
var target = arguments[0];
for(var i = 1, len = arguments.length; i < len; i++) {
for(var key in arguments[i]) {
target[key] = arguments[i][key];
}
}
return target;
}
关于for in遍历的补充
- 使用for in的方式遍历对象的属性,是无法遍历出js内置属性的。
使用属性copy的方式给原型添加属性的优点
- 不会覆写构造函数默认的prototype,那么对应的constructor属性就不会丢失
- 可以替代原型组合式的写法
- 使用灵活简单
3 -- 原型链
原型的规律
- 原型链的终点统一是Object.prototype
- 对象的原型和该对象的类型有关
- 比如Person的实例,原型是Person.prototype
- 比如Animal的实例,原型是Animal.prototype
- []的原型链结构
- [] ==> Array.prototype ==> Object.prototype ==> null
- {}的原型链结构
- {} ==> Object.prototype ==> null
- /abc/的原型链结构
- /abc/ ==> RegExp.prototype ==> Object.prototype ==> null
- Person的原型链结构
- Person ==> Function.prototype ==> Object.prototype ==> null
- Function的原型链结构
- Function ==> Function.prototype ==> Object.prototype ==> null
- Object的原型链结构
- Object ==> Function.prototype ==> Object.prototype ==> null
- 构造函数默认的prototype,它统一都继承Object.prototype
- 比如Person.prototype,原型是Object.prototype
- 比如Animal.prototype,原型是Object.prototype
- 通过这个规则,可以自由猜想出任意一个实例所有的原型
- 比如Book的实例,其原型结构为: Book实例 ==> Book.protoype ==> Object.prototype ==> null
原型链
- 一个对象,所有由__proto__联系在一起的原型,称之为这个对象的原型链。
如何研究一个对象的原型链结构
- 先通过__proto__得到对象的原型
- 然后访问这个原型的constructor属性,确定该原型的身份
- 然后继续按照上诉两个步骤,往上研究原型,最终就得到了对象的原型链。
instanceof -- 运算符
- 作用:判断一个对象的原型链中是否含有某个构造函数的prototype
- 语法:对象 instanceof 构造函数
- 返回值:boolean
hasOwnProperty -- 方法
- 作用:判断一个属性是不是自己的(不包含继承的属性)
- 语法:对象.hasOwnProperty(属性名)
- 返回值:boolean
in -- 运算符
- 作用:判断能否使用某个属性(包含继承的属性)
- 语法:属性名 in 对象
- 返回值:boolean
delete -- 运算符
- 作用:删除对象的属性
- 语法:delete 对象.属性名 || delete 对象[属性名]
- 返回值:boolean
Function -- 内置构造函数
- 作用:创建函数实例
- 语法:new Function(形参1,形参2,...,代码体)
- 返回值:新创建的函数实例
- 特点:能够把字符串当做js脚本执行
eval -- 内置的全局函数
- 作用:执行字符串代码
4 -- 作用域
作用域
变量的有效范围。
如何检测变量的有效范围
- 在指定的作用域下访问该变量,如果不报错,就证明这个变量的有效范围覆盖了这个作用域。
全局变量
- 指的是在代码的任何地方都可以使用的变量
在js中如何定义全局变量
- 在函数外定义
- 或者不使用var定义的变量(这种方式不标准,尽量不要使用)
局部变量
- 在变量定义的局部可以使用的变量
在js中如何定义局部变量
- 在函数内定义
变量的生命周期
- 全局变量的生命周期从定义开始,到页面关闭结束
- 局部变量的生命周期通常是从定义开始(函数被调用),到函数执行完毕结束(但是局部变量的生命周期可能因为闭包的存在被延长)
块级作用域 ==> js未采纳
- 凡是代码块就可以产生新的作用域,代码块内的变量外界无法使用。
函数作用域 ==> js采纳
- 只有函数可以产生新的作用域,函数内的变量外界无法使用。
- js中是没有块级作用域的,只有函数作用域。
词法作用域(静态作用域) ==> js采纳
- 查找一个变量,优先找函数自己作用域内的变量,找不到就去定义该函数的作用域中去找, 按照这个规则直到全局都没有找到,就报错。
动态作用域 ==> js未采纳
- 查找一个变量,优先找函数自己作用域内的变量,找不到就去调用该函数的作用域中去找, 按照这个规则直到全局都没有找到,就报错。
有一个容易搞混,又没有什么联系的知识点,这里强调一下
- 函数内的this,与函数的定义无关,与调用有关。
var obj = {
fn: function() { console.log(this) };
};
var fn = obj.fn;
// 同一个fn,三种调用方式,this分别不同
obj.fn(); // obj
fn(); // window
new fn(); // fn实例
- 变量的查找,与函数的定义有关,与调用无关。
function fn() {
console.log(a); // 报错,自己找不到,去定义fn的全局找,所以这里和fn的定义有关,与fn的调用无关。
}
(function() {
var a = 10;
fn();
})();
作用域的产生
- 函数可以被多次重复调用,调用一次就会产生一个新的作用域,每一个新作用域内会有新的变量。
作用域链
- 函数在定义的时候,将来它执行时的上级作用域就被确定好了,上级作用域可能还有上级,函数所有的上级作用域称之为作用域链。
- 一个函数作用域可以访问的所有上级作用域,称为它的作用域链。
5 -- 预解析&闭包
预解析
- 可以理解为js解析引擎在逐行执行代码前,对一些特殊代码的预先执行。
- 也可以认识是在马拉松之前的热身运动。
- 具体一点讲,是js在逐行执行代码前,会对js脚本进行一个整体检查。
- 1、检测语法有没有错误
- 2、变量声明提升:检测到变量声明那就率先进行声明(实际上是开辟一个内存空间,将来备用)
- 3、函数声明提升:检测到函数声明也率先进行声明(实际上是开辟两个内存空间,一个是变量,一个是函数)
- 预解析造成js一个特殊的象限,就是在变量声明和函数声明之前的代码,可以访问它们。
- js预解析完毕之后,才会整体正式逐行执行,但是预解析过的变量声明和函数声明不会重复执行。
- js预解析分为两种,全局预解析(全局代码执行的时候会先预解析)与局部预解析(函数在调用的时候内部的代码会先预解析)
变量声明
- 使用通过var定义的变量,才属于变量声明
- 例如:var a; 属于变量声明。
- 例如:b = 10; 不属于变量声明。
- var关键字可以通过逗号连续声明多个变量
- 例如:var a, b, c = 20, d = 30;
- a,b,c,d全部属于声明。
- var关键字在声明变量的时候,可以给其赋值,如果赋值表达式中含有一些变量,这些变量不属于变量声明。
- 例如:var a = b = 10;
- 其中a属于变量声明,b不属于。
函数的定义方式
- 字面量
- 函数声明
- function fn(){}
- 函数表达式
- var fn = function(){}
- 函数声明
- 构造函数
- new Function()
函数声明
在js中,函数声明式写法比较单一,好区分。
- 一定是以function关键字开头定义的函数
- 一定具有函数名
- 函数声明有两种,1种是全局函数声明,1中是局部函数声明
- 函数声明要么在全局,要么直接嵌套在另一个函数内
函数表达式
在js中,函数表达式的编写形式,多种多样。 比如把函数当作数据赋值给变量,或者把函数作为返回值return,或者当做参数传递,或者运算符运算,或者自调函数。
- 要么不是以function关键字开头来定义的函数,要么该函数定义在了语句当中
- 函数名可有可无
预解析细节规则
- 变量声明重名 -- 后面的忽略,没有必要定义重复的变量
- 函数声明重名 -- 保留后面的,因为函数体可能不一样,后面的优先与前面的
- 变量与函数重名 -- 保留函数
- 写在代码块中的函数,名字会被预解析,函数体不会
- 最终造成的现象是,在该函数定义的访问它,不会报错,得到一个undefined
console.log(fn) // undefined
if(true) {
function fn(){}
}
console.log(fn) // 函数体
- 函数表达式不会被预解析,但是函数表达式定义的函数执行时,其内部会对自己进行函数声明。
- 最终造成的现象是,在该函数的外面无法通过其名称找到它,但是在内部可以。
console.log(fn); // 报错
var a = function fn(){
console.log(fn); // 函数体,因为表达式定义的函数,会在自己内部被声明一次。
}
console.log(fn); // 报错
函数执行时形参的赋值
- 一个函数在执行时,会优先定义形参,然后赋值。
- 预解析和逐行执行都慢与形参。
(function(a) {
console.log(a); // 100
var a = 200;
console.log(a); // 200
}(100));
闭包
- 有权访问非自身局部变量(非全局变量)的函数,称为闭包。
- 有权访问自由变量(非全局变量)的函数,称为闭包。
自由变量
- 一个函数可以访问的非自身内部变量,称为这个函数的自由变量。
引用了自由变量的闭包特点
- 会延长自由变量的生命周期,只要闭包不死我就不死
闭包的应用
- 可以利用闭包的结构去管理一些重要的变量,防止外界随意对其修改
6 -- 函数的四种调用模式
this的特点
- 函数中的this,调用方式不同,指向不同
- this与调用有关,与定义无关
函数调用模式
- 函数名() || (function(){}()) ==> window
方法调用模式
- 对象.方法名() || 对象方法名 || 祖对象.父对象.子对象.方法名() ==> 宿主对象
构造器调用模式
- new 构造函数() || new 对象.构造函数() ==> new出来的新实例
间接调用模式(上下文调用模式)
- call
- 函数.call(指定的this,实参1,实参2,...)
- 对象.方法.call(指定的this,实参1,实参2,...)
- apply
- 函数.apply(指定的this,[实参1,实参2,...])
- 函数.apply(指定的this,{0: 实参1, 1:实参2, length: 2})
- 对象.方法.apply(指定的this,[实参1,实参2,...])
call和apply的使用范例
// 方法借用 -- 给伪数组对象添加属性值
var obj = {};
Array.protype.push.call(obj, '要添加的第一个值', '要添加的第二个值')
// 方法借用 -- 获取对象类型
var arr = [];
Object.prototype.toString.call(new Date).slice(8, -1)
// 方法借用 -- 借用父类构造函数给子类实例添加属性
function Parent(name, age) {
this.name = name;
this.age = age;
}
function Son() {
Parent.apply(this, arguments);
}
var p = new Son('火星人', 999);
// apply拆分数组或伪数组值依次传递给函数
var arr = [1, 10, 20, 40];
Math.max.apply(null, arr)
7 -- ES5新特性&设计模式&js高级补充
ES5数组新增的3个方法
forEach
- 作用:帮我们遍历数组,每遍历到一个值,就会调用一次回调,把这个值与它的下标传递过去
- 语法:数组.forEach(function(v, i){ console.log('使用forEach帮我们遍历好的值与下标') })
- 返回值:undefined
map
- 作用:可以用来代替forEach,但是map可以接收回调的返回值,最终通过一组数据映射为回调返回的另外一组数据
- 语法:var mapArr = 数组.map(function(v, i){ return v * v })
- 返回值:回调所有的返回值组成的新数组
filter
- 作用:可以用来代替forEach,但是还可以过滤数组中的值
- 语法:var filterArr = 数组.filter(function(v, i){ if(v % 2 ==0){ return true; } })
- 返回值:所有返回回调返回true的对应值组成的新数组
call&apply的补充
- 如果不传参 ==> this指向window
- 传null ==> this指向window
- 传undefined ==> this指向window
- 传123 ==> this指向123的包装类型对象(Number对象)
- 传'abc' ==> this指向'abc'的包装类型对象(String对象)
- 传true ==> this指向true的包装类型对象(Boolean对象)
- 传对象 ==> this指向传入的对象
构造函数的返回值
- 如果构造函数没有return语句,那么new它,得到一个新实例
- 如果构造函数return了一些基本类型数据,那么new它,得到一个新实例
- 如果构造函数return了一个对象,那么new它,得到return的对象
严格模式
- ES5新增的一个特性,使用该特性可以让js以一种新的模式运行js脚本。
- 该模式下可以强制我们抛弃那些不推荐不友好的写法
- 该模式下可以让js之前的一些设计不太合理的api表现的合理一些
- 该模式下可以让js拥有一些新的特性,比如ES6/ES7规范中定义的某些语法,必须在严格模式下才有效
严格模式的分类
- 全局模式
- 在全局代码的最上面书写一句话'use strict';
- 使用该模式,所有的代码都按照严格模式执行
- 局部模式
- 在函数内部的最上面书写一句话'use strict';
- 使用该模式,只有该函数内的代码才会按照严格模式执行
需要记住的几条严格模式规则
- 定义变量必须使用var
- 函数调用模式this为undefined
- 真正实现了call谁this就为谁
- eval拥有了单独的作用域
设计模式
沙箱模式
- 使用某种方式,防止一些代码对外界环境造成潜在影响,这类代码就可以认为是沙箱模式
- 在js中,最简单的沙箱模式写法,就是使用一个自调函数把某块代码进行封装,可以防止全局变量污染
工厂模式
- 凡是调用一个函数,函数返回一个对象,那么这个函数就可以认为是一个工厂。
- 在js中,一般的工厂都是把new对象的过程进行了一个封装。
单例模式
- 只要让某种类型的实例,只能存在一个,实现这种需求的代码就是单例模式
- 在js中,JSON与Math对象就被设计为单例模式,我们不能够创建第二个这种类型的对象。
观察者模式 -- 发布订阅模式
- 只要某事件发生后,会自动执行预设好的回调,那么实现这种需求的代码就是观察者模式
- 观察者模式可以认为就是自定义事件
- 观察者模式通常使用的场景是这样的:某对象做了某件事,其他对象做出相应的一个响应
bind
- ES5提供了一个新的可以改变函数this指向的新函数
- 作用:通过某函数得到一个绑定了固定this的新函数,这个新函数可以是旧函数的clone版本
- 语法:var bindFn = 函数.bind(this)
- 返回值:绑定了this的函数
类成员&实例成员
- 类成员
- 添加给类自己的属性与方法
- 实例成员
- 添加给实例自己的属性与方法
- 原型上供实例使用的属性与方法