2019年9月

这两天一直在搜集关于JS的原型方面的知识,今天我来总结一下关于原型和原型链等周边相关知识,在这里做一个易于理解的解释。
如果文章有误区或者有错误,请评论欢迎指出;

首先原型是什么?

原型是js继承的基础,js的继承就是原型的继承

Function的原型对象

我们先来看一张图,画的比较草率

微信图片_20190929202953.jpg

首先我们创建了一个函数person,那我们通过打印这个函数,会发现里面有一个prototype这个属性,那么这个prototype指的是这个person函数的原型,那么我们的函数原型中有一个构造函数(construtor)指向的就是person函数

那么我们就知道了函数中的prototype就是原型,原型里面的构造函数指的其实就是我们声明的函数;
搞清楚这个关系之后,我们可以通过new操作符去创建一下构造函数指向新的对象:

var obj = new person();

打印这个obj显示的是_proto_,这个_proto_其实指的就是person中的原型,那么_proto_中的构造函数也就是person构造函数(印证了我们上一条的观点)


1. 当new了构造函数之后,和原构造函数没有什么关系了,它只和原构造函数中的原型有关系;
2. 当我们new了多个构造函数之后,它们的对象的隐式原型都指向原构造函数的原型,因此它们共享原型中的属性和方法;
3. 当我们访问对象中的属性的时候,如果对象本身没有,回去对象中的原型中去查询,如果都没找到则返回未定义,如果对象本身和原型中有同样的属性和方法,会返回对象中的不会返回原型中的(查找的过程就叫原型链
4. 我们在对象中想这样修改原型中的值是不可能的:p1.name = "test"; 它只能读取原型中的值不能修改,这样操作是在本身的对象中添加一个name属性


证明它们

// 声明一个函数
function person(){};
person.prototype.name = "原型中的name";
person.prototype.age = "原型中的age";
// 1. new构造函数得出的新对象共享原型的属性和方法
var person1 = new person();
var person2 = new person();
console.log("new构造函数得出的新对象共享原型的属性和方法:" + person1.name); // 原型中的    name
console.log("new构造函数得出的新对象共享原型的属性和方法:"+ person1.name); // 原型中的name
// 2. 对象添加/修改属性不会操作原型中的
person1.others = "对象中的others";
console.log("对象添加/修改属性不会操作原型中的:" + person1.others); // 对象中的others
// 3. 对象和原型中存在同一个属性会返回对象中的
person1.name = "对象中的name";
console.log("对象和原型中存在同一个属性会返回对象中的:" + person1.name); // 对象中的name


关于原型的一些属性/方法

prototype属性: 存在于任何函数(不仅仅是构造函数,其他函数我们不关注而已),指向的是函数中的原型;
construtor属性:指向的是原构造函数;
  function person(){};  
    console.log(person.prototype.construtor === person); // true
    var person1 = new person();
    console.log(person1 instanceof person); // true

如果要重新赋值新的prototype,比如这样:

person.prototype = {namespace: "1"};

那么会造成这样的情况:

var person1 = new person();
console.log(person.prototype.construtor === person); // false

为了避免这样的情况(构造函数指向不正确):

person.prototype = {construtor: person, namespace: "1"};


_proto_:隐式原型

所有对象都有一个隐式原型,指向了构造函数的原型,这个原型不可访问,只有在谷歌浏览器,firefox浏览器支持直接._proto_这样的方式去访问,尽量开发者不要操作_proto_,因为不慎会影响继承原型链;

var person1 = new person();
console.log(person1._proto_ === person.prototype); // true

hasOwnProperty:判断属性是否来自对象本身/还是继承了原型中的属性
var person1 = new person();
person1.name = "shenhao";
console.log("name属性是不是来自对象本身的呢?" + person1.hasOwnProperty("name")); // true
console.log("age属性是不是来自对象本身的呢?" + person1.hasOwnProperty("age")); // false
console.log("sex属性是不是来自对象本身的呢?" + person1.hasOwnProperty("sex")); // false

我们可以看到,hasOwnProperty只能判断对象中的属性,不能判断原型中的属性,而且如果对象中的属性如果为空那么也会返回false;

那么我们如何判断这个属性是原型上的属性呢?

in操作符:查找对象中的属性,如果查找不到会在原型中去查询
var person1 = new person();
person1.name = "shenhao";
person.prototype.sex = "男"
console.log("sex" in person1); // true

但是有一个问题,in操作符查询对象中的属性,如果找到了就会返回对象中的,就不会去查询原型中的了,所以我们这个方案也失败;

所以我们可以用2者结合的方式去写一个函数:

function hasProto(obj, context){
    if(!(context in obj)){
        // 如果原型和对象中都没有属性
        console.log(`${context}不存在${obj}的原型和对象上`)
    }else if(obj.hasOwnProperty(context)){
        console.log(`${context}存在${obj}的对象上`)
    }else {
        // in操作符查询了(成功),但是hasOwnProperty查找却是失败,那么此属性在原型中存在
        console.log(`${context}存在${obj}的原型上`)
    }
}

创建对象的几种“模式”

原型的模式,如果在原型中创建了几个属性和方法,所有的构造函数出来的对象都会共享它们,方法共享比较nice,但是属性共享往往是不符合业务的,所以这是它最大的一个缺陷

构造函数的模式:每一个构造函数都会创建自己独立的属性和函数,属性自己独一份这没有错误,但是可能同样的函数要写很多份,势必会造成浪费;

但是在构造函数的模式,我们可以对function进行一个封装

function person(){
    this.name = "shenhao";
    this.age = "12";
    this.story = story;
};

function story(){
    return "一个开心的故事"
}

var person1 = new person();
console.log(person1.story()); //一个开心的故事

作为面向对象的语言,为了性能把方法从对象抽离出来,是非常emmmmm的,你们懂的

组合模式: 封装构造函数(传参)+ 原型模式
function person(name, age){
    this.name = name;
    this.age = age;
};

person.prototype.story = function(context){
    return context;
}

var person1 = new person("shenhao", "18");
console.log(person1.story("123"));
动态原型模式: 更优的解决方案,把构造函数和原型都写在了构造函数中
// 声明一个函数
function person(name, age){
    this.name = name;
    this.age = age;
    if(typeof this.story !== "function"){
        // 说明是第一次创建这个对象, 在原型上绑定
        person.prototype.story = (context) => {
            return this.name + context
        }
    }
};

var person1 = new person("shenhao", "18");
console.log(person1.story("你大爷"));


ok, 全部结束,希望你能有所收获

我们需要补充一些前置知识,比如单线程模型是什么?
js是一个单线程的脚本语言,之所以为什么不是多线程而是单线程,是因为历史遗留的原因,脚本语言如果使用了多线程,那么一个线程操作了dom,第二个线程也操作了dom,那么浏览器改听谁的,如果是多线程会对开发者来说是一个弊大于利的事情;

那么没有了多线程就说明了,我们的任务需要在一个线程中进行,但是js虽然是单线程,但是还有很多线程,只是同一时间执行事件的线程只有一个,这个线程叫做主线程

但是我们会发现,如果现在是单线程,执行任务要等到上一个任务执行结束才会到下一个,这对于一些IO操作,ajax请求操作是非常坑的事情,我们需要等到这些任务执行结束才会执行下面的,等待IO/ajax返回并不需要消耗CPU,还需要花时间等它们,非常不划算,所以js引入了新的概念叫做消息队列(任务队列)

js在执行任务的时候,会把所有的同步任务优先执行,把所有的异步任务挂起到其他队列,等到我们的同步任务全部清空,再看异步任务是否满足条件再添加到主线程中的任务队列中执行,比较经典的例子就是setTimeout;

let timer = setTimeout(() => {
    console.log("这并不是准时的延迟噢")
}, 1000)

延时器的意思就是我需要等待一段时间把这样的任务放到主线程的最后面,那么这个参数传递的是1000,并不是真的1秒延迟执行,而是在主线程前面的任务执行完毕,如果前面有很多耗时的任务,那么这个1000指的就是最少时间而不是最终时间;

当我们的同步任务被主线程全部执行完毕,会去检查其他的队列中存在异步任务,如果检查出来异步任务满足条件那么就放到了主线程去执行,那么这个异步任务也就变成了同步任务,然后当主线程又一次清空了,又要去找异步任务去执行,如果一旦任务队列是空的,那么程序执行结束。每一个消息会与一个函数进行一个关联,等到执行到此任务(消息)的时候,会执行对应的函数,如果没有这个函数,那么这个消息就会遗失;

那么我们把这样的一次一次的去查询异步任务是否满足条件进入主线程执行任务的这个过程称之为事件循环机制

维基百科对事件循环机制定义是这样的

“Event Loop是一个程序结构,用于等待和发送消息和事件(a programming construct that waits     for and dispatches events or messages in a program)”。可以就把Event Loop理解成动态更新的消    息队列本身。

可以用代码这样表示

while(queue.waitForMessage()){
    queue.processNextMessage();
}


那么任务可以分为2类:

  1. 宏观任务 (同步) :
    没有被引擎挂起,在主线程中执行的任务,只有前面的任务执行完毕才能轮到下一个
  2. 微观任务 (异步)
    被js引擎挂起的任务,这些任务会在任务队列中,只有js引擎认为有条件进入主线程,那么就加入到主线程摇身一变就变成了同步任务,排在异步任务后面的代码,不用等前面的执行,会立即执行;

关于单线程问题,除了主线程,还有ajax线程,setTimeout线程,事件监听线程,这也都证明了js是单线程处理事件的语言,而不是只有一个线程;

20170804013026998.png

今天做题遇到了一个问题,vue的模板语法是基于什么的,带着强烈的好奇心我找到了它,mustache,胡子语法;
然后打开了官网,这个语法已经支持了20多种编程语言,js的支持是node的作者开源的;

微信截图_20190909220549.png

git地址: https://github.com/janl/mustache.js

模板引擎技术是非常有用的,所以它不是一个冷门的知识,反而是需要我们去了解的;

所以我们通过github的安装指令,一步一步的来实现基本的api

我搜查了很多资料,网上并没有一个使用npm包的方式来做demo,都是使用render来执行渲染,鉴于我们日常工作中使用npm比较多,我做一版npm的demo

首先npm init初始化一个空项目;

然后安装mustache

npm i mustache -s -d

在package.json中填写如下内容:

"scripts": {
    "build": "mustache dataView.json myTemplate.mustache>public/output.html",
  },

如同这个指示看到的一样,我们需要创建一个json文件,这个文件就是变量配置文件,还需要创建一个模板文件,这个模板文件相当于执行render函数的文件,这种方式更加一目了然;

我们创建它们,我使用了vscode,并且装了相关的mustache的插件,所以语法会有提示;

微信截图_20190909221112.png

我们首先在json文件中写入一个对象,里面写一个值,然后在模板文件中使用{{}}来执行渲染;

然后执行

npm run build

就会发现在public下面生成了一个html文件,如果报错,说明你的文件目录跟我的不一样

我们在json文件中,写入了这么多值,有普通的值,有布尔值,有数组等等

{
"name": "shenhao",
"age": "19",
"html": "<p>123</p>",
"isTrue": true,
"thisIsObject": {
    "name": "shenhao",
    "age": "19"
},
"isArray": [{
    "name" : "shenhao"
},{
    "name" : "shenhao"
},{
    "name" : "shenhao"
}]
}

我们在模板中写出了这些代码,我在模板中写了一个简单的html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
{{name}} 有一个 {{&html}}  
<br>
<hr>
 {{thisIsObject.name}} {{thisIsObject.age}}
 <br>
<hr>
-webkit- {{#isTrue}} 如果是真就显示了 {{/isTrue}}
 <br>
<hr>
循环一下下面的内容, 如果是数组,可以用.来表示循环的每一个元素
{{#isArray}} {{name}} {{/isArray}}
<br>
<hr>
{{!^}}与{{!#}}相反,如果变量是null、undefined、 false、和空数组讲输出结果

</body>
</html>

我们来讲解一下基本的api

{{name}}:会在json中查询对应的值,并且渲染
{{&html}}: html在json中如果式一个html标签,可以用这样的方式进行转义 (类似vue中的v-html)
{{#boolean}} 和 {{/boolean}}: 是一个组合,如果boolean为真那么它们之间的内容会渲染,否则不会
{{^boolean}}: 和上面用法一样,只不过是上面的else
{{object.name}}: 同样支持对象键值对的方式获取
{{#array}} 和 {{/array}}: 如果这样写是一个数组,那么不仅有判断boolean的真假,它会迭代中间可以写迭代中的每一个元素,每一个元素可以用{{.}}来获取,如果要获取迭代中的内容是一个键值对,那么可以直接使用{{name}}

这就是mustache简单的用法,上面有demo,你们可以对着demo敲一遍就能非常easy的理解了;
希望理解了这个技术,你可以在其他语言同样可以用到它!