面向对象
面向对象编程(Object Oriented Programming , OOP) 是一种编程范式,它将代码分为具有属性和方法的对象。这种方式的好处是:将相关代码片段封装到对象中,由对象来维护在程序里面的生命状态。对象可以按需被复用或者被修改。
编程范式
在此之前呢先介绍一下,什么叫做编程范式。
所谓编程范式(programming paradigm),指的是计算机编程的基本风格或典范模式。借用哲学的属于,如果说每一个编程者都在创造虚拟世界,那么编程范式就是他们置身其中自觉不自觉采用的世界观和方法论。
在我们的编程语言里面,根据编程范式来分类大致可以分为两大类:命令式编程和声明式编程
1.命令式编程
一直以来,比较流行的都是命令式编程。所谓命令式编程,就是以命令为主,给机器提供一条又一条的命令序列让其原封不动的执行。程序执行的效率取决于执行命令的数量。
一句话概括:命令式编程就是命令**"机器"如何去做事情,这样不管你想要的是什么(what),它都会按照你的命令实现。**
我们常见的命令式编程有C语言,C++,Java,C#
2.声明式编程
所谓声明式编程就是告诉**“机器” 你想要什么,让机器想出如何去做(how)**
在声明式编程里面又可以分为2个大类:领域专用语言和函数式编程。
领域专用语言:
英语全称为 domain specific language ,简称SDL。主要是指对一些对应专门领域的高层编程语言,和通用编程语言的概念相对。DSL对应的专门领域(Domain)一般比较狭窄,或者对应某个行业,或者对于某一类具体应用程序,比如数据库等。我们常见的有HTML、CSS、SQL等。
函数式编程:
所谓函数式编程,简单来讲,就是一种编程模型,将计算机运算看作是数学中函数的计算。在函数式编程中,函数是基本单位,是第一型,它几乎被用做一切,包括最简单的计算,甚至连变量都别计算索取代。在函数式编程中,变量只是一个名称,而不是一个存储单元,这是函数式编程与传统的命令式编程最典型的不同之处。
随着web开发中高并发的需求越来越多,多核并行的程序涉及被推倒前线,而传统的命令式编程天生的缺陷使得并行编程模型变得非常复杂,使程序员不堪其重。函数式编程在解决兵法程序上面却有着天然的优势,代码简洁优雅,但是缺点就是学习门槛高。
面向对象
一、面向对象基本特征
- 封装:也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
- 继承:通过继承创建的新类称为“子类”或“派生类”。继承的过程,就是从一般到特殊的过程。
- 多态:对象的多功能,多方法,一个方法多种表现形式。
- Javascript是一种基于对象(object-based)的语言。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)—–es6以前是这样的。所以es5只有使用函数模拟的面向对象。
二、对象实例化方式
- 原始模式:这样的写法有两个缺点,一是如果多生成几个(100个!)实例,写起来就非常麻烦;二是实例与原型之间,没有任何办法,可以看出没有什么联系。
var Car = {
color: 'red',//车的颜色
wheel: 4,//车轮数量
}
var Car2 = {
color: 'blue',
wheel: 4,
}
alert(Car.color);//red
- 原始模式的改进:通过写一个函数,解决代码重复的问题。
function createCar(color,wheel) {
return {
color:color,
wheel:wheel
}
}
//然后生成实例对象,就等于是在调用函数:
var cat1 = createCar("红色","4");
var cat2 = createCar("蓝色","4");
alert(cat1.color);//红色
- 工厂模式
function createCar(color,wheel){//createCar工厂
var obj = new Object;//或obj = {} 原材料阶段
obj.color = color;//加工
obj.wheel = wheel;//加工
return obj;//输出产品
}
//实例化
var cat1 = createCar("红色","4");
var cat2 = createCar("蓝色","4");
alert(cat1.color);//红色
- 构造函数模式:为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。 所谓”构造函数”,其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。加new执行的函数构造内部变化:自动生成一个对象,this指向这个新创建的对象,函数自动返回这个新创建的对象
function CreateCar(color,wheel){//构造函数首字母大写
//不需要自己创建对象了
this.color = color;//添加属性,this指向构造函数的实例对象
this.wheel = wheel;//添加属性
//不需要自己return了
}
//实例化
var cat1 = new CreateCar("红色","4");
var cat2 = new CreateCar("蓝色","4");
alert(cat1.color);//红色
三、构造函数注意事项
- 此时CreateCar称之为构造函数,也可以称之类,构造函数就是类 。
- cat1,cat2均为CreateCar的实例对象。
- CreateCar构造函数中this指向CreateCar实例对象即 new CreateCar( )出来的对象。
- 必须带new 。
- 构造函数首字母大写,这是规范,官方都遵循这一个规范,如Number() Array()。
- contructor:这时cat1和cat2会自动含有一个constructor属性,指向它们的构造函数,即CreateCar。
alert(cat1.constructor == CreateCar); //true
alert(cat2.constructor == CreateCar); //true
- 每定义一个函数,这个函数就有一个 prototype 的属性{},proto 指向被实例化的构造函数的prototype,prototype默认带constructor属性,constructor指向构造函数。
instanceof 运算符:object instanceof constructor运算符,验证构造函数与实例对象之间的关系。
alert(cat1 instanceof CreateCar ); //true
alert(cat2 instanceof CreateCar ); //true
四、构造函数的问题
构造函数方法很好用,但是存在一个浪费内存的问题。如果现在为其再添加一个方法showWheel。那么,CreateCar就变成了下面这样,这样做有一个很大的弊端,对于每一个实例对象,showWheel都是一模一样的内容,每一次生成一个实例,都必须生成重复的内容,多占用一些内存。这样既不环保,也缺乏效率。
function CreateCar(color,wheel){
this.color = color;
this.wheel = wheel;
this.showWheel = function(){//添加一个新方法
alert(this.wheel);
}
}
//还是采用同样的方法,生成实例:
var cat1 = new CreateCar("红色","4");
var cat2 = new CreateCar("蓝色","4");
alert(cat1.showWheel == cat2.showWheel); //false
五、Prototype 原型
Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。 这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上。__proto__是原型链,指向实例化的函数原型。
function CreateCar(color,wheel){
//属性写构造函数里面
this.color = color;
this.wheel = wheel;
}
//方法写原型里面
CreateCar.prototype.showWheel = function(){
alert(this.wheel);
}
CreateCar.prototype.showName = function(){
alert('车');
}
//生成实例。
var cat1 = new CreateCar("红色","4");
var cat2 = new CreateCar("蓝色","4");
cat1.showName();//'车'
//这时所有实例的showWheel属性和showName方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。
alert(cat1.showWheel == cat2.showWheel );//true
alert(cat1.showName == cat2.showName );//true
console.log(cat1.__proto__ === CreateCar.prototype); //true
六、对象和函数的关系
对象是由函数构造出来的。
Object是Function 的一个实例。
Object.constructor == Function //true
函数是Function 的实例,但不是Object 的实例。
function fn(){}
fn.constructor == Function //true
fn.constructor == Object //false
{} 与 Object 的关系。
var obj = {};
obj.constructor === Object //true
七、静态方法和静态属性
只属于类而不属于实例化对象
function foo(){
this.show = function(){
return this;
}
}
foo.test = 123; //静态属性
foo.say = function(){
return this;
}
foo.say();
var fn = new foo(); //实例化的新的对象,this指向这个新的对象,不能访问类的静态方法
fn.say(); //Noname1.html:45 Uncaught TypeError: fn.say is not a function
console.log(foo.say() == fn.say());
八、对象继承
利用call()及for in继承 。
给对象的constructor.prototype添加方法属性,对象就会继承,如果要实现一个对象继承其他对象,采用如下方法。
//人类
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.run = function(){
console.log('跑路~')
};
Person.prototype.say = function(){
console.log('说话~')
};
console.log(Person.prototype);
//男人
function Man(){
this.sex = "男";
}
Man.prototype = Person.prototype;
Man.prototype.yyy = function(){
console.log('嘤嘤嘤');
}
//会发现Person的prototype也改变了,因为复杂对象的赋值操作是引用而不是赋值
console.log(Person.prototype);
//人类
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.run = function(){
console.log('跑路~')
};
Person.prototype.say = function(){
console.log('说话~')
};
console.log(Person.prototype);
//男人
function Man(){
this.sex = "男";
}
for(var key in Person.prototype){
Man.prototype[key] = Person.prototype[key];
console.log(key)
}
Man.prototype.yyy = function(){
console.log('嘤嘤嘤');
}
console.log(Person.prototype);
var xm = new Man();
xm.yyy();
采用中介
function ClassA(name){
this.name = name;
}
ClassA.prototype.say = function(){
console.log(666);
}
//中继来做准备工作
function Ready(){}//
Ready.prototype = ClassA.prototype;//引用
//需要来继承ClassA
function ClassB(){}
ClassB.prototype = new Ready();//new 返回了一个新对象 __proto__指向被实例化的构造函数的prototype
ClassB.prototype.constructor = ClassB;
console.log(ClassB.prototype);
采用中介,使用call改变this指向
function ClassA(name){
this.name = name;
}
ClassA.prototype.showName = function(){
console.log(this.name);
}
//中继来做准备工作
function Ready(){}//
Ready.prototype = ClassA.prototype;//引用
//需要来继承ClassA
function ClassB(name){
ClassA.call(this,name);
}
ClassB.prototype = new Ready();//new 返回了一个新对象 __proto__指向被实例化的构造函数的prototype
ClassB.prototype.constructor = ClassB;
console.log(ClassB.prototype);
var xiaoming = new ClassB('小明');
xiaoming.showName();
九、多态
同一个方法,面对不同的对象有不同的表现形式就叫做多态。
var obj = {
eat : function(_type){
if(_type == '猫'){
console.log('猫粮')
}else if (_type == "狗") {
console.log('狗粮')
}else{
console.log("吃饭");
}
}
};
obj.eat("狗");
十、hasOwnProperty
查看该属性是否在这个对象本身上,只有在自身属性上才会返回真,在原型链上会返回假。
function ClassA(){}
ClassA.prototype.test = function(){
console.log('test')
}
var a = new ClassA();
a.test();
console.log(a.hasOwnProperty('test')); //false
十一、描述符(修饰符)
描述符是对一个属性的特性的描述,defineProperty设置描述符(修饰符),value设置属性值,configurable是否允许修饰符被改变 默认为false,enumerable 是否可以被枚举 默认为false,writable 是否可以被 = 等号改变 默认为false。
var obj = {
a : 1
};
var c = 666;
Object.defineProperty(obj,'c',{
//value : 233,
//enumerable : false,
//writable : true,//他的值能否改变
//设置的时候调用
set : function(n){
//n 就是等号的右边的值
c = c*n;
},
//获取的时候调用
get : function(){
return c;
},
configurable : true,//是否可以再次修改修饰符
});
面向对象总结
- 所谓编程范式,指的是计算机编程的基本风格或典范模式。大致可以分为命令式编程和声明式编程。
- 面向对象的程序设计是站在哲学的角度上,将人类思维融入到了程序里面的一种编程范式。
- 描述对象的时候可以通过两个方法来进行描述。分别是对象的外观和功能。在程序中与之对应的就是属性和方法。
- JavaScript中的对象都是基于原型的,从一个原型对象中可以克隆出一个新的对象。
- 在JavaScript中每个对象都有一个原型对象。可以通过
__proto__
属性来找到一个对象的原型对象。 - 在其他语言中,对象从类中产生。而JavaScript中,通过构造函数来模拟其他语言中的类。
- 类与对象的关系可以总结为:类是对对象的一种概括,而对象是类的一种具体实现。
- 面向对象的三大特征:封装、继承、多态。
- 封装是指对象的属性或者方法隐藏于内部,不暴露给外部。
- 继承是指一个子类继承一个父类。在继承了父类之后,子类就拥有了父类的所有属性和方法。
- 多态是指不同的对象可以拥有相同的方法,不过是以不同的方式来实现它。
- this的指向是根据使用的地方不同分为好几种情况,但是我们可以通过一些方式来修改this的指向。
执行上下文
- 先进后出,后进先出。
函数的执行上下文的生命周期
执行上下文的生命周期分为两个阶段:
- 创建阶段(进入执行上下文):函数被调用时,进入函数环境,为其创建一个函数上下文
- 执行阶段(代码执行):执行函数中代码时,此时执行上下文进入执行状态。
创建阶段的操作
- 创建变量对象
- 函数环境会初始化创建 Arguments 对象,形式参数(并赋值)。
- 普通函数声明(并赋值)
- 局部变量声明,函数表达式声明(未赋值)
- 初始化作用域链
- 确定this指向(this由调用者确定)
- 确定作用域(词法环境决定,哪里声明定义,就在哪里确定)
执行阶段的操作
- 变量对象赋值
- 变量赋值
- 函数表达式赋值
- 调用函数
- 按顺序执行其他代码
执行上下文与作用域区别
作用域和执行上下文不是同一个概念。
执行全局代码时,会产生一个执行上下文环境,每次调用函数都会执行上下文环境。当函数调用完时,这个上下文环境以及其中的数据都会被消除(除了闭包),处于活动状态的执行上下文环境只有一个
而作用域在函数定义时就已经确定了,不是在函数调用时确定(区别于执行上下文环境,当然this也是上下文环境里的成分)
// 全局作用域
let x = 100;
function bar() {
console.log(x);
}
// fn作用域
function fn() {
let x = 50; // bar作用域
bar();
}
fn(); // 100
作用域只是一个"地盘",其中没有变量。变量时哦通过作用域对应的执行上下文环境中的变量对象来实现的。所以作用域是静态观念的,而执行上下文环境是动态上的,两者并不一样。
变量对象
执行上下文抽象成为了一个对象,拥有个属性,分别是变量对象,作用域链以及this指向。
- 变量对象里面所拥有的东西
- Arguments对象
- 确定形式参数,检查上下文中函数声明
- 找到每一个函数声明
- 就在 variableObject 下用函数名创建一个属性,属性值就指向该函数在内存中的地址的一个引用。
- 确定当前上下文中的局部变量,如果遇到和函数名同名的变量,则会忽略该变量
- 通过例子来演示函数的这两个阶段以及变量对象是如何变化的。
const foo = function(i){
var a = "Hello";
var b = function privateB(){}; function c(){}
}
foo(10);
//首先在建立阶段
fooExecutionContext = { variavleObject : {
arguments : {
0 : 10,length : 1
}, // 确定arguments对象
i : 10, // 确定形式参数
c : pointer to function c(), // 确定函数的引用
a : undefined, // 局部变量 初始值为 undefined
b : undefined // 局部变量 初始值为 undefined
},
scopeChain : {},
this : {}
}
14-2闭包(closure)
闭包:
- 一个函数中要嵌套一个内部函数,并且内部函数要访问外部函数的变量
- 内部函数要被外部引用
例
function eat(){
var food = '鸡翅';
return function(){
console.log(food);
} }
var look = eat();
look(); // 鸡翅
look(); // 鸡翅
//eat函数返回一个函数,并在这个内部函数中访问food这个局部变量。调用eat函数并将结果look,这个look指向了eat函数中的内部函数,然后调用它,最终输出food值
自由变量
- 自由变量可以理解成跨作用域的变量,比如子集作用域访问作用域的变量。
例:
var school = function () {
var s1 = "小强"
var s2 = "小王";
var team = function (project) {
console.log(s1 + s2 + project);
}
return team;
} var team = school();
team("做电商项目"); // 小强小王做电商项目
team("做微信项目"); // 小强小王做微信项目
//变量s1和s2属于school函数的局部变量,并被内部哈桑农户team使用,同时该函数也被返回出去在外部,通过调用school得到了内部函数的引用,后面多次调用这个内部函数,仍然能够访问到s1和s2变量被team函数引用,即使创造它们的函数school执行完了,这些变量依然存在,这就是闭包
3:闭包的原理
因为作用域对象包含该上下文中的 VO/AO对象,还有scope对象,所以内部函数可以访问到函数的变量
4:闭包的优缺点
优点:
- 通过闭包可以让外部环境访问到函数内部的局部变量。
- 通过闭包可以让局部变量持续保存下来,不随着它的上下文环境一起销毁。
缺点: - 局部变量本来应该在函数退出时被解除引用,但如果局部变量被封印在闭包形成的环境中,那么这个局部变量就一直能生存下去,然而闭包的确会使一些数据无法被及时销毁。
函数节流
- 目的:是为了防止一个函数短时间内被频繁的触发。
- 原理:是将即将执行的函数用
setTimeout()
延迟一段时间执行。而函数节流的原理是让连续的函数执行,变为固定时间段间断的执行
1:使用时间戳
- 触发事件时,取出当前的时间戳,然后减去之前的时间戳(最一开始值设为0),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
例:
function throttle(func, wait) {
let context, args;
let previous = 0;
return function () {
let now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
2:使用定时器
- 触发事件时,设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,知道定时器执行,然后执行函数,清空定时器,这样就可以设置一个定时器。
例:
function throttle(func, wait) {
let timeout, args, context;
let previous = 0;
return function () {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args)
}, wait);
}
}
}
14-3(分时函数)
timeChunk()函数接受3个参数。第一个参数是创建节点时需要用到的数据,第二个参数是封装了创建节点逻辑的函数,第三个参数表示每一批创建的节点数量
<body>
<script>
const timeChunk = function (ary, fn, count) {
let obj, t;
let len = ary.length;
const start = function () {
for (let i = 0; i < Math.min(count || 1, ary.length); i++) { let obj = ary.shift();
fn(obj);
}
};
return function () {
t = setInterval(function () {
//如果全部节点都已经被创建好
if (ary.length === 0) {
return clearInterval(t);
}
start();
}, 200); //分批执行的时间间隔,也可以用参数的形式传入
};
};
//最后进行一些测试,假设我们有1000个好友的数据
// 然后利用timeChunk函数,每一批直往页面中创建8个节点
const ary = [];
for (let i = 1; i <= 1000; i++) {
ary.push(i);
}
const renderFriendList = timeChunk(ary, function (n) {
const div = document.createElement('div');
div.innerHTML = n;
document.body.appendChild(div);
}, 8);
renderFriendList();
</script>
</body>
可以看到1000条数据是分批创建添加的
##14-4(惰性函数)
在Web开发中,因为浏览器之间实现的差异,一些嗅探工作总是不可避免的,比如需要在各个浏览器中能够通用的事件绑定函数addEvent()
解决方式:
- 惰性载入函数方法
此时add Event()
依然被声明为一个普通函数,在函数里依然有一些分支判断。但是在第一次进入条件分支之后,在函数内部会重写这个函数,重写之后的函数就是我们所期望的add Event()
函数,在下一次进入时它不再存在条件分支
<body>
<div id="div1">点我绑定事件</div>
<script>
const addEvent = function (elem, type, handler) {
if (window.addEventListener) {
addEvent = function (elem, type, handler) {
elem.addEventListener(type, handler, false);
}
}
if (window.attachEvent) {
addEvent = function (elem, type, handler) {
elem.attachEvent('on' + type, handler);
}
}
addEvent(elem, type, handler);
// 第一次绑定事件的时候需要手动调用以下
};
const div = document.getElementById('div1');
addEvent(div, 'click', function () {
alert(1);
});
addEvent(div, 'click', function () {
alert(2);
});
</script>
</body>