关键词

面向对象、迭代器、模块化

面向对象

对象遍历

  1. for in
  2. Object.keys(xxx)把key取出来作为一个数组。
  3. Object.values(xxx)遍历对象的值作为一个数组

    编程思想

  • 基本思想是使用对象,类,继承,封装等基本概念来进行程序设计
  • 优点:
    • 易维护:采用面向对象思想设计的结构,可读性高,由于继承的存在,即使改变需求,维护也只是在局部模块,所以维护起来非常方便,成本较低
    • 易拓展
    • 开发工作的重用性,继承性高,降低重复工作量
    • 缩短了开发周期

      一般面向对象包括:继承,封装,多态,抽象

对象形式的继承

讲深浅拷贝之前,我们需要知道,基本数据类型在栈里面,被复制了就算修改也不会改变原来的的值,引用数据类型在堆里面,因为共享内存所以复制后被修改会改变源对象的值,至此引申出深浅拷贝。

  • 浅拷贝

    基本的浅拷贝就是对象的赋值,但我们需要注意,实际上我们的需求是复制源对象上面的属性,那么单纯的赋值,会把新对象的值给覆盖掉。参见下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var Person = {
name: 'allin',
age: 18,
address: {
home: 'home',
office: 'office',
},
sclools: ['x','z'],
};

var programer = {
language: 'js',
};
//如果不使用该方法 programmer里面的language会被覆盖掉
function extend(p, c){
var c = c || {};
for( var prop in p){
c[prop] = p[prop];
}
}
extend(Person, programer);
//or
// programer = Object.assign(programer,Person);
programer.name; // allin
programer.address.home; // home
programer.address.home = 'house'; //house
Person.address.home; // house
//programmer依旧保留自己的language

注意 浅拷贝不是新对象的地址指向整个旧对象的地址,而是拷贝旧对象的属性的地址。即自己原先的内容不变,拷贝的过程中如果key重复覆盖,否则保留。
思路是遍历对象 赋值。

深拷贝

深拷贝是开辟一个新的内存地址,将源对象的各个属性复制进去
注意的点:对象原型上面的属性不应该去拷贝,使用到Object.hasOwnProperty(key)

  1. 通过JSON.parse(JSON.stringfy())进行深拷贝
  • 是序列化和反序列化的过程,序列化是存储地址的一个映射,所以反序列化之后,修改并不会影响原先的地址。就可以达成深拷贝。
  • 缺点:不能拷贝undefined function 正则 Error对象
  1. 递归
  • 通过判断引用类型数据进行初始化之后赋值的操作指向新的地址。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function deepClone(obj){
    //判断是否是引用类型或者null 否则返回源对象进行浅拷贝
    if(typeof obj !== 'object'||obj===null){
    return obj;
    }
    //判断是否是数组,初始化地址。
    let result;
    if(obj instanceof Array){
    result = [];
    }else{
    result = {};
    }
    //递归调用
    for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
    result[key] = deepClone(obj[key]);
    }
    }
    return result;
    }
  • 缺点:栈会溢出。

封装

  • 命名空间
    • js是没有命名空间的,因此可以用对象来模拟
1
2
3
4
5
6
7
8
9
10
var app = {}//命名空间app
//模块1
app.module1 = {
name:'allin',
f:function(){
console.log('hi');
}
};
app.module1.name;
app.module1.f();

对象的属性外界是可读可写的,那么如何达到封装的目的?通过闭包和局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Girl(name,age){
//通过方法访问变量
var love = 'jojo';
this.name =name;
this.age = age;
this.say = function(){
return love;
}
this.movelove = function(){
love = 'aaa';
}
}
var g = new Girl('bb',19);
g.say();
g.movelove();
g.say();

静态成员

就是在函数外面定义静态方法,静态方法只有该类能够使用。

1
2
3
4
5
6
7
8
9
function Person(name){
var age = 100;
this.name = name;
}
//静态成员
Person.walk = function(){
console.log('static');
};
Person.walk(); // static

私有与公有

对象的方法和属性分为私有和公有,公有的属性需要在实例化的时候传入对应的值去调用,私有的属性和方法只能通过公有的方法暴露出去。注意私有的方法如果返回的是公有的属性,还需要使用call改变this指向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(id){
// 私有属性与方法
var name = 'allin';
var work = function(){
console.log(this.id);
};
//公有属性与方法
this.id = id;
this.say = function(){
console.log('say hello');
work.call(this);
};
};
var p1 = new Person(123);
p1.name; // undefined
p1.id; // 123
p1.say(); // say hello 123

多态

同一个父类继承出来的子类有各自的形态,写的时候注意子类,prototype = new 父类()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Cat(){
this.eat = '肉';
}

function Tiger(){
this.color = '黑黄相间';
}

function Cheetah(){
this.color = '报文';
}

function Lion(){
this.color = '土黄色';
}

Tiger.prototype = Cheetah.prototype = Lion.prototype = new Cat();//共享一个祖先 Cat

var T = new Tiger();
var C = new Cheetah();
var L = new Lion();

console.log(T.color);
console.log(C.color);
console.log(L.color);

console.log(T.eat);
console.log(C.eat);
console.log(L.eat);

抽象类

虚函数是类成员中的概念,是只做了一个声明而未实现的方法,具有虚函数的类称之为抽象类。抽象类不能被实例化因为其中的虚函数并不是一个完整的函数,不能被调用。
在js中实现抽象类就是在父类中调用一个未定义的方法,但这个方法在子类中必须被实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function AbstractClass(){
throw new Error('抽象类不能直接被调用')
}
AbstractClass.prototype.detect = function(){
console.log('start');
}
AbstractClass.prototype.stop = function(){
console.log('stop')
}
AbstractClass.prototype.init = function(){
throw new Error('error');
}
function NormalClass(){
NormalClass.prototype = Object.create(AbstractClass.prototype);
NormalClass.prototype.constructor = NormalClass;
}
var n = new NormalClass();
console.log(n)
n.detect();
n.init();

QA

  1. 面向对象的三个基本特征
  2. 手写一下函数的公有和私有方法以及调用的形式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Person(id){
    var name = 'jojo';
    var sayName = function(){
    return this.id;
    }
    this.id = id;
    this.say = function(){
    console.log('111');
    sayName.call(this);
    }
    }

Iterator迭代器

Iterator是一种接口,也可以说是一种规范,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署了Iterator接口,就可以完成遍历操作。即依次处理该数据结构的所有成员

语法:

1
2
3
const obj = {
[Symbol.iterator]:function(){}
}

[Symbol.iterator]属性名是固定的写法,只要拥有了该属性的对象,就能用迭代器的方式进行遍历。

  • 迭代器的遍历方法是首先获得一个迭代器的指针。初始时该指针指向第一条数据之前,接着通过调用next方法,改变指针的指向,让他调用下一条数据。
  • 每次的next都会返回一个对象 有两个属性
    • value: 表示你想获取的数据
    • done:布尔值 代表遍历是否结束 true则结束

      iterator的作用

      三个作用
  • 创建一个指针对象指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  • 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  • 第二次调用指针对象的next方法 指针指向第二个成元
  • 不断调用next 直到结束位置(假如有三个数据 需要next四次 最后一次的结果是{ value: undefined, done: true })
    案例
    1
    2
    3
    4
    5
    6
    let arr = [{nums:1},2,3];
    let it = arr[Symbol.iterator]();//获取数组的迭代器
    console.log(it.next()) // { value: Object { num: 1 }, done: false }
    console.log(it.next()) // { value: 2, done: false }
    console.log(it.next()) // { value: 3, done: false }
    console.log(it.next()) // { value: undefined, done: true }

    具有iterator接口的数据结构

    一个数据结构只要有iterator接口就能被认为是可以遍历的。可以用forof。

    具有iterator接口的数据结构的有四种:
  1. 数组
  2. 类数组
  3. Set
  4. Map

    为什么对象没有iterator接口

  • 对象只能用forin和Object.keys、values遍历
  • 因为一个对象的哪个属性先遍历和后便利是不确定的,需要开发者手动指定。
  • 对对象部署iterator接口没有必要 因为map弥补了他的缺陷而且map有iterator接口
    对对象部署iterator
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    let obj = {
    id: '123',
    name: '张三',
    age: 18,
    gender: '男',
    hobbie: '睡觉'
    }
    obj[Symbol.iterator] = function(){
    let keyArr = Object.keys(obj);
    let index = 0;
    return {
    next(){
    return index<keyArr.length?{
    value:{
    key:keyArr[index],
    val:obj[keyArr[index++]]
    },
    }:{
    done:true
    }
    }
    }
    }
    for (const key of obj) {
    console.log(key)
    }

模块化

四种方案:commonjs,AMD,CMD,ES6

  • CommonJS,node的,通过require来引入模块,通过module.exports定义输出接口。是以同步的方式引入模块的。

  • AMD是采用异步的方式加载模块,模块的加载不影响后面语句的执行 所有依赖这个模块的语句都定义在一个回调函数里面,等加载完再执行回调函数。requirejs实现了AMD规范

  • CMD方案,也是解决异步加载的委托。代表有seajs。和requirejs的区别在于模块定义的时候堆依赖的处理不同和对依赖模块的执行时机处理不同。
  • 最后是es6的,通过import和export进行导入导出。默认暴露统一暴露分别暴露。
    • 默认暴露:export default
    • 分别暴露:export xxx1;exportxxx2,引入import {xxx1,xxx2} from 'xxx'
    • 统一暴露:export {aa1,aa1},引入import {aa1,aa2} from 'xxx'

CommonJS

  • 写法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //a.js
    module.exports = {
    a:1
    }
    //or
    exports.a = 1;
    //b.js
    var module = require('./a.js');
    module.a;//1
  • 注意浏览器中使用的话用browserify

    和es6的区别如下:

  • 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案,前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。
  • 而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。
  • 但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  • 后者会编译成 require/exports 来执行的

AMD

和CMD的区别:

  • 对依赖的处理不一样。AMD是依赖前置。定义模块的时候就要声明依赖的模块,CMD就近依赖,用到哪个才去require
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// CMD
define(function(require, exports, module) {
var a = require("./a");
a.doSomething();
// 此处略去 100 行
var b = require("./b"); // 依赖可以就近书写
b.doSomething();
// ...
});

// AMD 默认推荐
define(["./a", "./b"], function(a, b) {
// 依赖必须一开始就写好
a.doSomething();
// 此处略去 100 行
b.doSomething();
// ...
})
  • 对于依赖模块的执行实际处理不同。AMD和CMD异步加载,但是AMD在模块加载完就立刻执行依赖模块,依赖模块的执行顺序和我们写的顺序不一定一致。而CMD在依赖模块加载完成后并不执行,只是下载。等全部的依赖模块都加载好后,再去执行,和我们的书写顺序一致。