胖蔡叨叨叨
你听我说

[前端面试题] JS面试题(一)

胖蔡阅读(9)

一、谈谈你对回流和重绘的理解?

  • 回流:当一个元素自身的宽高,布局,显示或隐藏,或元素内部的文字结构发生变化,导致需要重新构建页面的时候,就产生了回流复制
  • 重绘:当一个元素自身的宽高,布局,及显示或隐藏没有改变,而只是改变了元素的外观风格的时候,就产生了重绘。

当元素添加或者删除可见的DOM元素,当元素的位置发生改变,元素的尺寸发生改变,内容改变和页面第一次渲染的时候页面会进行回流。

当改变元素的color、background、box-shadow、visibility、background-size的时候页面会进行重绘。

二、JS数据类型有哪些?

基本数据类型:

  • Number
  • String
  • Boolean
  • Null
  • Undefined
  • Symbol
  • bigInt

引用数据类型:

  • object
  • Array
  • Date
  • Function
  • RegExp

三、如何判断js的数据类型?

  • typeof:可以用来区分除了 Null 类型以外的原始数据类型,对象类型的可以从普通对象里面
  • instanceofA instanceof B 可以判断A是不是B的实例,返回一个布尔值,由构造类型判断出数据类型。
  • 通过Object下的toString.call()方法来判断:Object.prototype.toString.call(true); // => "[object Boolean]"
  • 根据对象的contructor判断:arr.constructor === Array
  • 通过Jquery中判断数据类型:jQuery.isArray()

四、JS中创建对象的几种方法

1、{} 或者 new Object()

就是先创建对象,后通过赋值法给对象添加属性,如下:

 <script>
        'use strict'; //使用strict模式
        /**
        使用{}创建对象,等同于 new Object();
        **/
        var o = {};

        o.name = '胖蔡';
        o.sayName = function(){
            alert(this.name);
        }

        alert(o.name+'-'+o.age);

        o.sayName();

    </script>

2、使用字面量

象字面变量是对象定义的一种简写形式,举个例子:var person = {name: 'zhang', age:20}, 这就是字面量形式,完全等价于var person = {}; person.name='zhang'; person.age=20;

3、工厂模式创建对象

通过函数来创建对象,和上述并没有本质区别:

<script>
        'use strict';

        // 使用工厂模式创建对象
        // 定义一个工厂方法
        function createObject(name){
            var o = new Object();
            o.name = name;
            o.sayName = function(){
                alert(this.name);
            };
            return o;
        }

        var o1 = createObject('zhang');
        var o2 = createObject('li');

        //缺点:调用的还是不同的方法
        //优点:解决了前面的代码重复的问题
        alert(o1.sayName===o2.sayName);//false

    </script>

4、构造函数模式(constructor)

使用构造函数实现创建对象:

 <script>
        'use strict';

        /**
         *  构造函数模式创建对象
         **/
        function Person(name){
            this.name = name;
            this.sayName = function(){
                alert(this.name);
            };
        }

        var p1 = new Person('zhang');
        var p2 = new Person('li');

        p1.sayName();
        p2.sayName();

        alert(p1.constructor === p2.constructor);//true
        alert(p1.constructor === Person);//true

        alert(typeof(p1));//object

        alert(p1 instanceof Object); //true
        alert(p2 instanceof Object); //true

        alert(p1.sayName===p2.sayName);//false

    </script>

5、原型模式(prototype)

每个方法中都有一个原型(prototype),每个原型都有一个构造器(constructor),构造器又指向这个方法。

  <script>
        'use strict';

        /*
         *  原型模式创建对象
         */
        function Animal() { }

        Animal.prototype.name = 'animal';
        Animal.prototype.sayName = function () { alert(this.name); };

        var a1 = new Animal();
        var a2 = new Animal();

        a1.sayName();

        alert(a1.sayName === a2.sayName);//true
        alert(Animal.prototype.constructor);//function Animal(){}
        //修改a2.name,a1的name不会变
        a2.name = 'dog';
        a2.sayName();//dog
        a1.sayName();//animal
    </script>

这个方法和上述类似,对象的创建均是使用构造函数实现的,但对象属性是通过 JS 的原型来进行创建的。通过原型创建对象,把属性和方法绑定到prototype上,通过这种方式创建对象,方法是共享的,每个对象调用的是同一个方法。

如果往新建的对象中加入属性,那么这个属性是放在对象中,如果存在与原型同名的属性,也不会改变原型的值。但是访问这个属性,拿到的是对象的值。

访问的顺序:对象本身>构造函数的prototype

如果对象中没有该属性,则去访问prototype,如果prototype中没有,继续访问父类,直到Object,如果都没有找到,返回undefined

6、构造函数+原型模式

这种方式结合上面两种方法,代码如下:

 <script>
        'use strict';

        function Animal(name){
            this.name = name;
            this.friends = ['dog','cat'];
        }
        Animal.prototype.sayName = function(){
            alert(this.name);
        };
        var a1 = new Animal('d');
        var a2 = new Animal('c');
        a1.friends.push('snake');
        alert(a1.friends);//[dog,cat,snake]
        alert(a2.friends);//[dog,cat]

  </script>

五、简单介绍下JS闭包,什么是函数柯里化?

闭包是指函数可以访问其外部上下文中定义的变量,即使这些变量在闭包函数执行时已经销毁。闭包可以创建私有变量并且可以在不同的函数调用中维持它们的值。在JavaScript中,每次函数调用都会创建一个新的执行上下文环境,闭包需要在其执行环境中创建一个持久化环境以保存其外部上下文的状态。

function counter() {
  let count = 0;
  return function increment() {
    count++;
    console.log(count);
  }
}
var c = counter();
c(); // 输出1
c(); // 输出2
c(); // 输出3

函数柯里化是通过将一个多参数的函数转换成一个多个单参数函数的过程,例如将函数f(x,y)转换成函数f(x)(y)。这个过程可以用闭包来实现。柯里化的用途包括简化函数调用、提供默认参数值以及延迟求值等。示例代码如下:


    function fn(a, b) {
      return function inner(type) {
        let res
        switch (type) {
          case '+': res = a + b; break;
          case '-': res = a - b; break;
          case '*': res = a * b; break;
          case '/': res = a / b; break;
        }
        return res
      }
    }

    // f 接受到的就是 inner 函数
    var f = fn(100, 200)
    console.log(f('+'))
    console.log(f('-'))
    console.log(f('*'))
    console.log(f('/'))

由于闭包的内部函数可以访问函数外部参数,所以有可能导致本该回收的变量没有被回收,导致内存泄漏,所以不能滥用闭包。

六、JS中的作用域是什么?作用域链又是什么?

代码(变量)可以使用的范围就是作用域。主要是为了提高程序的可靠性,也是为了减少命名冲突:

  • 全局作用域:指的是整个js文件,定义在全局中的全局变量,可以在局部作用域中使用,函数内部没有声明直接赋值的变量也叫全局变量
  • 局部作用域:主要指的是函数作用域,函数内部也就是局部作用域

在函数内部var定义的变量,叫做局部变量,局部变量无法被外部获取。

作用域链是指JavaScript在查找变量的时候,先从当前作用域开始查找,如果没有找到则向上一级作用域查找,直到找到为止。理解作用域链可以帮助我们更好地理解变量的作用范围及可访问性。例如,当我们在一个函数中访问一个变量时,JavaScript会先查找当前函数内的变量,如果没有找到则向上一级作用域查找,直到找到为止。如果一直找到全局作用域还没有找到,则会返回undefined

七、如何修改this的指向?

this指针都是当前运行的函数执行上下文,也可以理解为一个变量。当为了实现的需求,我们需要对某些运行的代码修改this指向,这时就可以使用JavaScript中提供的修改this指向的三种方法:callapplybind

  • call、bind、apply 都是 JavaScript 中用于改变函数执行上下文(即 this 指向)的方法。
  • call apply 的作用是一样的,都是用来调用函数并且改变函数内部的 this 指向。区别在于传参的方式不同,call 的参数是一个一个传递的,而 apply 的参数是以数组的形式传递的。
  • bind 方法不会立即执行函数,而是返回一个新的函数,这个新的函数的 this 值被绑定到了指定的对象,调用时也可以传入参数。同时使用 bind 方法可以实现柯里化,即将函数转化为接收部分参数的函数。

八、什么是事件冒泡?怎么阻止事件冒泡?

当我们点击子元素触发父元素的事件,这种现象,我们叫做事件冒泡,即由子元素向祖先元素传播,就像气泡从水底上浮,我们可以使用event.stopPropagation();阻止事件冒泡。

  • event.stopPropagation(); // 阻止了事件冒泡,但不会阻击默认行为。
  • event.preventDefault(); // 阻止默认事件,比如a的跳转事件。

九、简单说下深拷贝和浅拷贝

  • 浅拷贝:基本数据类型拷贝的是值,引用数据类型拷贝的是地址
  • 深拷贝:引用数据类型拷贝的是开辟新地址中的值

使用场景可以在数据传值的时候,当需要进行数据传值的时候,使用...或者Object.assign进行数据复制的时候,使用的就是浅拷贝,通过JSON.stringify、JSON.parse或者直接通过对象遍历进行实现深拷贝。

十、防抖与节流

  • 防抖:前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,只会执行最后一次。
  • 节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发。

防抖的原理是:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。节流的原理是:规定在一个单位时间内,只能触发一次函数,可以配合setTimeout、clearTimeout来使用。如果这个函数时间触发多次函数,只有一次有效。和防抖函数差不多。

十一、请解释一下 JavaScript 的同源策略

JavaScript的同源策略(Same-Origin Policy)是一种安全机制,用于保护Web浏览器中的文档对象模型(DOM)免受恶意网站的跨域攻击。同源策略要求在访问Web页面中的资源时,只能与来源相同的资源进行交互,来源由协议、主机名和端口号组成。

具体来说,以下条件满足时,就认为两个URL具有相同的源:

  • 协议(Protocol):两个URL的协议必须相同,如http://http://,或https://https://
  • 主机名(Host):两个URL的主机名必须相同,如example.comexample.com
  • 端口号(Port):如果URL中指定了端口号,两个URL的端口号必须相同,如example.com:80example.com:80。如果URL中没有指定端口号,则默认为80(对于HTTP)或443(对于HTTPS)。

十二、什么是跨域?如何解决跨域?

跨域:指浏览器同源策略限制的请求。跨域解决方法如下:

1、jsonp

浏览器的script、img、iframe标签是不受同源策略限制的 ,所以通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的callback函数,并把把我们需要的json数据作为参数传入。在服务器端,当req.params参数中带有callback属性时,则把数据作为callback的参数执行,并拼接成一个字符串后返回。

  • 兼容性好,在很古老的浏览器中也可以用,简单易用,支持浏览器与服务器双向通信。 
  • 只支持GET请求,且只支持跨域HTTP请求这种情况(不支持HTTPS

2、iframe

H5提供了postMessage()的方法,可以在父子页面进行通信,iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。这个APIwindow对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

3、服务器配置CORS

服务器端通过检查请求头部的origin,从而决定请求应该成功还是失败。具体的方法是在服务端设置Response Header响应头中的Access-Control-Allow-Origin为对应的域名,实现了CORS(跨域资源共享),这里出于在安全性方面的考虑就是尽量不要用 *,但对于一些不重要的数据则随意,例如图片。
CORS需要后端配置,前端不需要处理。

4、中间件代理(nodejs等)

通过一个nodejs服务来进行跨域处理,网页数据直接通过中间件获取,这样获取的数据就都是同源的了。

5、websocket

websocket可以不需要考虑跨域问题,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client server 之间的双向通信就与 HTTP 无关了。

十三、JavaScript中的垃圾回收机制

JavaScript中的内存回收机制主要依靠自动垃圾回收(Garbage Collection)来管理内存。垃圾回收器定期扫描内存中的对象,并标记那些不再被引用的对象,然后释放它们所占用的内存空间。

  • 标记-清除(Mark and Sweep):这是最常用的垃圾回收算法。垃圾回收器首先标记所有从根对象(如全局对象、活动执行上下文、当前调用栈等)可访问到的对象。然后,它遍历所有对象并清除未被标记的对象,即释放它们所占用的内存。
  • 引用计数(Reference Counting):这种算法会为每个对象维护一个引用计数器。当对象被引用时,计数器加1;当对象的引用被移除时,计数器减1。当计数器为零时,表示对象不再被引用,即可被回收。然而,引用计数算法难以处理循环引用的情况,导致循环引用的对象无法被回收。

现代JavaScript引擎通常使用标记-清除算法作为主要的垃圾回收机制。除此之外,还使用了一些优化技术来改进回收性能,如分代回收、增量回收和空闲时间回收等。

Python Tesseract库尝试实现验证码识别

胖蔡阅读(242)

机器视觉

从 Google 的无人驾驶汽车到可以识别假钞的自动售卖机,机器视觉一直都是一个应用广 泛且具有深远的影响和雄伟的愿景的领域。

我们将重点介绍机器视觉的一个分支:文字识别,介绍如何用一些 Python库来识别和使用在线图片中的文字。

我们可以很轻松的阅读图片里的文字,但是机器阅读这些图片就会非常困难,利用这种人类用户可以正常读取但是大多数机器人都没法读取的图片,验证码 (CAPTCHA)就出现了。验证码读取的难易程度也大不相同,有些验证码比其他的更加难读。

将图像翻译成文字一般被称为光学文字识别(Optical Character Recognition, OCR)。可以实现OCR的底层库并不多,目前很多库都是使用共同的几个底层 OCR 库,或者是在上面 进行定制。

ORC库概述

在读取和处理图像、图像相关的机器学习以及创建图像等任务中,Python 一直都是非常出色的语言。虽然有很多库可以进行图像处理,但在这里我们只重点介绍:Tesseract

Tesseract

Tesseract 是一个 OCR 库,目前由 Google 赞助(Google 也是一家以 OCR 和机器学习技术闻名于世的公司)。Tesseract 是目前公认最优秀、最精确的开源 OCR 系统。 除了极高的精确度,Tesseract 也具有很高的灵活性。它可以通过训练识别出任何字体,也可以识别出任何 Unicode 字符。

安装Tesseract

Windows 系统

下载可执行安装文件https://code.google.com/p/tesseract-ocr/downloads/list安装。

Linux 系统

可以通过 apt-get 安装: $sudo apt-get tesseract-ocr

Mac OS X系统

用 Homebrew(http://brew.sh/)等第三方库可以很方便地安装 brew install tesseract

要使用 Tesseract 的功能,比如后面的示例中训练程序识别字母,要先在系统中设置一 个新的环境变量 $TESSDATA_PREFIX,让 Tesseract 知道训练的数据文件存储在哪里,然后搞一份tessdata数据文件,放到Tesseract目录下。
  • 在大多数 Linux 系统和 Mac OS X 系统上,你可以这么设置: $export TESSDATA_PREFIX=/usr/local/share/Tesseract
  • 在 Windows 系统上也类似,你可以通过下面这行命令设置环境变量: #setx TESSDATA_PREFIX C:\Program Files\Tesseract OCR\Tesseract

安装

Tesseract 是一个 Python 的命令行工具,不是通过 import 语句导入的库。安装之后,要用 tesseract 命令在 Python 的外面运行,但我们可以通过 pip 安装支持Python 版本的 Tesseract库:

pip install pytesseract

文字处理

你要处理的大多数文字都是比较干净、格式规范的。格式规范的文字通常可以满足一些需求,不过究竟什么是“格式混乱”,什么算“格式规范”,确实因人而异。 通常,格式规范的文字具有以下特点:

  • 使用一个标准字体(不包含手写体、草书,或者十分“花哨的”字体) • 虽然被复印或拍照,字体还是很清晰,没有多余的痕迹或污点
  • 排列整齐,没有歪歪斜斜的字
  • 没有超出图片范围,也没有残缺不全,或紧紧贴在图片的边缘

文字的一些格式问题在图片预处理时可以进行解决。例如,可以把图片转换成灰度图,调 整亮度和对比度,还可以根据需要进行裁剪和旋转(详情请关注图像与信号处理),但是,这些做法在进行更具扩展性的 训练时会遇到一些限制。

格式规范文字的理想示例

通过下面的命令运行 Tesseract,读取文件并把结果写到一个文本文件中: `tesseract test.jpg text

cat text.txt 即可显示结果。

识别结果很准确,不过符号^*分别被表示成了双引号和单引号。大体上可以让你很舒服地阅读。

通过Python代码实现

import pytesseract
from PIL import Image

image = Image.open('test.jpg')
text = pytesseract.image_to_string(image)
print text

运行结果:

This is some text, written in Arial, that will be read by
Tesseract. Here are some symbols: !@#$%"&*()

对图片进行阈值过滤和降噪处理(了解即可)

很多时候我们在网上会看到这样的图片:

Tesseract 不能完整处理这个图片,主要是因为图片背景色是渐变的,最终结果是这样:

随着背景色从左到右不断加深,文字变得越来越难以识别,Tesseract 识别出的 每一行的最后几个字符都是错的。

遇到这类问题,可以先用 Python 脚本对图片进行清理。利用 Pillow 库,我们可以创建一个 阈值过滤器来去掉渐变的背景色,只把文字留下来,从而让图片更加清晰,便于 Tesseract 读取:

from PIL import Image 
import subprocess

def cleanFile(filePath, newFilePath): 
    image = Image.open(filePath)

    # 对图片进行阈值过滤,然后保存
    image = image.point(lambda x: 0 if x<143 else 255)     
    image.save(newFilePath)

    # 调用系统的tesseract命令对图片进行OCR识别     
    subprocess.call(["tesseract", newFilePath, "output"])

    # 打开文件读取结果
    file = open("output.txt", 'r')     
    print(file.read()) 
    file.close()

cleanFile("text2.jpg", "text2clean.png")

通过一个阈值对前面的“模糊”图片进行过滤的结果

除了一些标点符号不太清晰或丢失了,大部分文字都被读出来了。Tesseract 给出了最好的 结果:

从网站图片中抓取文字

用 Tesseract 读取硬盘里图片上的文字,可能不怎么令人兴奋,但当我们把它和网络爬虫组合使用时,就能成为一个强大的工具。

网站上的图片可能并不是故意把文字做得很花哨 (就像餐馆菜单的 JPG 图片上的艺术字),但它们上面的文字对网络爬虫来说就是隐藏起来 了,举个例子:

  • 虽然亚马逊的 robots.txt 文件允许抓取网站的产品页面,但是图书的预览页通常不让网络机 器人采集。
  • 图书的预览页是通过用户触发 Ajax 脚本进行加载的,预览图片隐藏在 div 节点 下面;其实,普通的访问者会觉得它们看起来更像是一个 Flash 动画,而不是一个图片文 件。当然,即使我们能获得图片,要把它们读成文字也没那么简单。
  • 下面的程序就解决了这个问题:首先导航到托尔斯泰的《战争与和平》的大字号印刷版 1, 打开阅读器,收集图片的 URL 链接,然后下载图片,识别图片,最后打印每个图片的文 字。因为这个程序很复杂,利用了前面几章的多个程序片段,所以我增加了一些注释以让 每段代码的目的更加清晰:

import time
from urllib.request import urlretrieve 
import subprocess
from selenium import webdriver
#创建新的Selenium driver
driver = webdriver.PhantomJS()

# 用Selenium试试Firefox浏览器:
# driver = webdriver.Firefox()

driver.get("http://www.amazon.com/War-Peace-Leo-Nikolayevich-Tolstoy/dp/1427030200")
# 单击图书预览按钮 driver.find_element_by_id("sitbLogoImg").click() imageList = set()
# 等待页面加载完成
time.sleep(5)
# 当向右箭头可以点击时,开始翻页
while "pointer" in driver.find_element_by_id("sitbReaderRightPageTurner").get_attribute("style"):
    driver.find_element_by_id("sitbReaderRightPageTurner").click()
    time.sleep(2)
    # 获取已加载的新页面(一次可以加载多个页面,但是重复的页面不能加载到集合中) 
    pages = driver.find_elements_by_xpath("//div[@class='pageImage']/div/img") 
    for page in pages:
        image = page.get_attribute("src")
        imageList.add(image)
driver.quit()

# 用Tesseract处理我们收集的图片URL链接 
for image in sorted(imageList):
    # 保存图片
    urlretrieve(image, "page.jpg")
    p = subprocess.Popen(["tesseract", "page.jpg", "page"], stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    f = open("page.txt", "r")
    p.wait() print(f.read())

和我们前面使用 Tesseract 读取的效果一样,这个程序也会完美地打印书中很多长长的段 落,第六页的预览如下所示:

6
     "A word of friendly advice, mon
     cher. Be off as soon as you can,
     that's all I have to tell you. Happy
     he who has ears to hear. Good-by,
     my dear fellow. Oh, by the by!" he
     shouted through the doorway after
     Pierre, "is it true that the countess
     has fallen into the clutches of the
     holy fathers of the Society of je-
     sus?"

     Pierre did not answer and left Ros-
     topchin's room more sullen and an-
     gry than he had ever before shown
     himself.

但是,当文字出现在彩色封面上时,结果就不那么完美了:

   WEI' nrrd Peace
   Len Nlkelayevldu Iolfluy
   Readmg shmdd be ax
   wlnvame asnossxble Wenfler
   an mm m our cram: Llhvary
    - Leo Tmsloy was a Russian rwovelwst
    I and moval phflmopher med lur
    A ms Ideas 01 nonviolenx reswslance m 5 We range     0, "and"

如果想把文字加工成普通人可以看懂的 效果,还需要花很多时间去处理。

下一节将介绍另一种方法来解决文字混乱的问题,尤其是当你愿意花一点儿时间训练 Tesseract 的时候。

通过给 Tesseract 提供大量已知的文字与图片映射集,经过训练 Tesseract 就可以“学会”识别同一种字体,而且可以达到极高的精确率和准确率,甚至可以忽略图 片中文字的背景色和相对位置等问题。

验证码处理

许多流行的内容管理系统即使加了验证码模块,其众所周知的注册页面也经常会遭到网络 机器人的垃圾注册。

那么,这些网络机器人究,竟是怎么做的呢?既然我们已经,可以成功地识别出保存在电脑上 的验证码了,那么如何才能实现一个全能的网络机器人呢?

大多数网站生成的验证码图片都具有以下属性。

  • 它们是服务器端的程序动态生成的图片。验证码图片的 src 属性可能和普通图片不太一 样,比如 <img src="WebForm.aspx?id=8AP85CQKE9TJ">,但是可以和其他图片一样进行 下载和处理。
  • 图片的答案存储在服务器端的数据库里。
  • 很多验证码都有时间限制,如果你太长时间没解决就会失效。
  • 常用的处理方法就是,首先把验证码图片下载到硬盘里,清理干净,然后用 Tesseract 处理 图片,最后返回符合网站要求的识别结果。
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import requests
import time
import pytesseract
from PIL import Image
from bs4 import BeautifulSoup

def captcha(data):
    with open('captcha.jpg','wb') as fp:
        fp.write(data)
    time.sleep(1)
    image = Image.open("captcha.jpg")
    text = pytesseract.image_to_string(image)
    print "机器识别后的验证码为:" + text
    command = raw_input("请输入Y表示同意使用,按其他键自行重新输入:")
    if (command == "Y" or command == "y"):
        return text
    else:
        return raw_input('输入验证码:')

def zhihuLogin(username,password):

    # 构建一个保存Cookie值的session对象
    sessiona = requests.Session()
    headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0'}

    # 先获取页面信息,找到需要POST的数据(并且已记录当前页面的Cookie)
    html = sessiona.get('https://www.zhihu.com/#signin', headers=headers).content

    # 找到 name 属性值为 _xsrf 的input标签,取出value里的值
    _xsrf = BeautifulSoup(html ,'lxml').find('input', attrs={'name':'_xsrf'}).get('value')

    # 取出验证码,r后面的值是Unix时间戳,time.time()
    captcha_url = 'https://www.zhihu.com/captcha.gif?r=%d&type=login' % (time.time() * 1000)
    response = sessiona.get(captcha_url, headers = headers)


    data = {
        "_xsrf":_xsrf,
        "email":username,
        "password":password,
        "remember_me":True,
        "captcha": captcha(response.content)
    }

    response = sessiona.post('https://www.zhihu.com/login/email', data = data, headers=headers)
    print response.text

    response = sessiona.get('https://www.zhihu.com/people/maozhaojun/activities', headers=headers)
    print response.text


if __name__ == "__main__":
    #username = raw_input("username")
    #password = raw_input("password")
    zhihuLogin('xxxx@qq.com','ALAxxxxIME')

值得注意的是,有两种异常情况会导致这个程序运行失败。第一种情况是,如果 Tesseract 从验证码图片中识别的结果不是四个字符(因为训练样本中验证码的所有有效答案都必须 是四个字符),结果不会被提交,程序失败。第二种情况是虽然识别的结果是四个字符, 被提交到了表单,但是服务器对结果不认可,程序仍然失败。

在实际运行过程中,第一种 情况发生的可能性大约为 50%,发生时程序不会向表单提交,程序直接结束并提示验证码 识别错误。第二种异常情况发生的概率约为 20%,四个字符都对的概率约是 30%(每个字 母的识别正确率大约是 80%,如果是五个字符都识别,正确的总概率是 32.8%)。

如何使用JQuery进行网站开发?

胖蔡阅读(401)

JQuery是较早一代的前端开发的利器, 极大地简化了 JavaScript 编程。就目前而言也是大多数网站使用的工具,就轻量型网站开发选择JQuery可以帮助我们快速实现网站的开发工作。本篇文章主要介绍如何使用JQuery进行简单的网站开发。

如何引入JQuery?

jquery就是通过JS的方式来操作界面的DOM,极大的简化我们操作DOM的复杂度。所以,应用方式也是一样的,代码示例如下:

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>

不同的是,我们可以根据jquery的来源分为两种类型:

1、本地jquery引入

这种方式就是将jquery的文件放在本地(或者说服务器地址),我们通过地址访问jquery,这种方式的引用优势是三方库可控性大大加强,jquery由我们自己保管维护。

<script src="//domain/jquery/3.4.1/jquery.min.js"></script>

2、CDN引入

CDN可以将一个地方的资源文件分布在不同的网络位置,极大的加快我们的访问速度,节省加载时间,这里介绍几个常用的Jquery免费CDN地址。

  • jQuery官网

jquery官方给我们提供了各个版本的jquery CDN地址,但由于外网环境,我们这边可能还是下载比较慢,可以考虑做英文站,海外服务器的时候使用。参考地址可以访问:https://releases.jquery.com/。使用示例如下:

<script src="http://code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
  • 微软CDN
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.0.min.js"></script>
  • CDNjs

CDNjs是用于专门提供前端CDN服务使用,但同样也是国外的CDN服务,大家慎重选择。

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>

除了jquery,cdnjs中也可以引用其他公用库的CDN服务。

  • jsDeliver

jsDeliver是目前用的比较多的一个CDN服务,虽然服务商是国外的,但是并不太影响国内的使用(目前看来),和上方的CDNjs一样的,它本身也是一个CDN服务提供商,我们也可以使用它来代理请求我们其他需要使用的前端库,大家可以访问地址:https://www.jsdelivr.com/,搜索需要的npm库,jquery使用如下:

<script src="https://cdn.jsdelivr.net/npm/jquery@3.6/dist/jquery.min.js"></script>
  • 新浪CDN

新浪CDN可以通过http://lib.sinaapp.com/?path=/jquery 来查找你需要的具体版本Jquery,新浪提供了jquery各个版本的压缩版和未压缩版,可以自己选择使用,使用代码如下:

<script src="http://lib.sinaapp.com/js/jquery/1.10/jquery-1.10.0.min.js"></script>

另注:现有网上说的百度 Jquery CDN已经不可使用了,所以这里不列出来了。

3、版本选择

这里采用网上已有的一个方案进行选择,分属电脑端和手机端选择不同的jquery版本。选择建议如下:

  • 电脑端:为了兼容IE8,建议使用jquery 1.9版本,因为1.9之后jquery就宣布不再兼容IE浏览器了,若无IE兼容要求,可以直接使用jquery最新版本。
<script src="http://lib.sinaapp.com/js/jquery/1.9.1/jquery-1.9.1.min.js"></script>
  • 手机端:建议使用jquery最新版本,新版本的jquery对于性能和使用方式上都有很大的改善。

如何使用JQuery?

jquery引入和会在window下挂载一个$和JQuery对象,我们通过如下几个方面了解JQuery使用:

1、进入加载

// 等同于onload函数
$(function () {   
    ...  // 此处是页面 DOM 加载完成的入口
}) ; 
 

// 等同于ready函数
$(document).ready(function(){
   ...  //  此处是页面DOM加载完成的入口
});

2、jquery选择器

jquery最神奇和令人惊叹的就是jquery可以通过选择器快速选择html元素和操作:

//筛选ul中li的一个
$("ul li:first").css("color", "#ff0000");
//筛选ul中li的下标值为2的
$("ul li:eq(2)").css("color", "blue");
//筛选ul中li中下标值为奇数的
$("ol li:odd").css("color", "skyblue");
//筛选ul中li中下标值为偶数的
$("ol li:even").css("color", "pink");

3、事件绑定

通过jquery我们可以快速绑定键盘和鼠标事件,示例代码如下:

<script>
	var node = document.getElementById("xxx");
	$(node).css("color","red");//把原生的节点对象,转换成了jQuery对象
		
	//传统方式
	node.onclick = function() {}
	
	//法一:事件名(函数)  即事件名作为 方法名来用
	$("#btn").click(function() {console.log("点击了我");});
	$("#btn").mouseover(function() {console.log("over");});
	$("#btn").mouseout(function() {console.log("out")});
		
	//法二:bind('事件名', 函数)绑定
	$("#btn").bind('click', function() {console.log("点击");});
	$("#btn").bind('mouseover', function() {console.log("鼠标上来...")});
	$("#btn").bind('mouseout', function() {console.log("鼠标出去...")});
			
</script>

JavaScript中let、const和var的使用介绍

胖蔡阅读(335)

varJavaScriptES6之前使用的变量定义关键字。通过var声明的变量,会挂载在当前定义的函数执行上下文。若在最外层定则变量就挂载在全局执行上下文,即window对象下。而const和let则是ES6中提出的定义变量或常量关键字,其中const用于声明常量,let则是用于声明局部变量关键字。

var

var定义的变量可以修改,不强制要求初始化,未初始化则输出undefined。简单使用如下:

var a = 1; //此处声明的变量a为全局变量
function foo(){
   var a = 2;//此处声明的变量a为函数foo的局部变量
   console.log(a);//2
}
foo();
console.log(a);//1

特点:

  • 存在变量提升
  • 可多次声明,后面声明会覆盖之前的声明
  • 函数声明var变量会有隔离的效果,这时该变量为局部变量,外部无法访问

let

let是ES6之后提出的用于声明局部变量。其作用域为代码块区域,let的使用示例如下:

let a = 1;
console.log(a);//1
console.log(b);//Uncaught ReferenceError: b is not defined
let b = 2;
function foo(){
    let a = 1;
    let a = 2;//Uncaught SyntaxError: Identifier 'a' has already been declared
}

特点:

  • let作用域块状作用域,如函数内或代码块内,如for循环内
  • 同一个代码块内不允许重复声明
  • 不影响作用域链
  • 不同于var的是let不存在变量提升

const

const是和let一样在ES6之后别加入的关键词,const被设计专门用于声明常量,示例如下:

const a = 1;
console.log(a);//1
a = 2;
console.log(a);//Uncaught TypeError: Assignment to constant variable.

特点:

  • 块级作用域,和let一样
  • 不允许作用域内重复声明
  • 值不允许修改,对于引用类型而言不可修改引用类型指向,但可以修改引用类型内部属性值

前端开发中网络请求方式汇总

amiko阅读(480)

我们去聊前端开发,就绕不过网络请求,可以说网络请求支撑起来了前端的半边天,因为有网络数据的交互,才使得我们的网页变得更加“灵活”起来了,让我们的网站能及时反馈数据给用户。本篇文章主要介绍在前端开发中,我们常用的几种网络请求方式,首先来了解下常用的网络请求方式有哪几种:

  • ajax请求
  • axios请求
  • fetch请求
  • xhr原生请求
  • websocket请求

Ajax请求

ajax是由Jquery库提供的一种网络异步请求方法,通过ajax我们可以快速获取后端请求数据,并将其包装输出到页面元素上。请原理实现还是通过xhr封装请求服务器,通过服务器返回数据进行DOM渲染,我找到一个比较能描述的流程图像。

图1.1 ajax原理图

基础使用

# 格式
$.ajax({name:value, name:value, ... }) // 可以直接传入url 


# 使用示例
$.ajax({
    url:"http://api.mediastack.cn/base/captcha",
    dataType:"json",
    error:function(error){
         console.log("get catch error:",error)
    },
    success:function(result){
        // 后台成功返回
        console.log("get result:",result)
    }
});

ajax请求的字典值支持如下:

  • url:字符串,请求的url
  • async:布尔值,表示请求是否异步处理。默认是 true
  • beforeSend(xhr):发送请求前回调函数。
  • data:发送到服务器的数据。将自动转换为请求字符串格式。GET 请求中将附加在 URL 后面。
  • dataType:字符串类型, 预期服务器返回的数据类型。如:
    • xml:返回 XML 文档
    • html:返回纯文本 HTML 信息
    • script:把响应的结果当作 JavaScript 执行。
    • json:把响应的结果当作 JSON 执行,并返回一个JavaScript对象。
    • jsonp: 以 JSONP 的方式载入 JSON 数据块。会自动在所请求的URL最后添加 “?callback=?“。
    • text:返回纯文本字符串。
  • context:这个对象用于设置Ajax相关回调函数的上下文。
  • converters :一个数据类型到数据类型转换器的对象。每个转换器的值是一个函数,返回经转换后的请求结果。默认为{"* text": window.String, "text html": true, "text json": jQuery.parseJSON, "text xml": jQuery.parseXML}
  • crossDomain:如果你想在同一域中强制跨域请求(如JSONP形式),例如,想服务器端重定向到另一个域,那么需要将crossDomain设置为 true 。默认同域请求为false, 跨域请求为true
  • contentType:发送信息至服务器时内容编码类型。默认值是”application/x-www-form-urlencoded; charset=UTF-8“,适合大多数情况。
  • complete:请求完成后回调函数 (请求success 和 error之后均调用)。这个回调函数得到2个参数: jqXHR (在 jQuery 1.4.x中是 XMLHTTPRequest) 对象和一个描述请求状态的字符串("success""notmodified""error""timeout""abort", 或者 "parsererror") 。
  • cache:布尔类型,默认为true.如果设置为 false ,浏览器将不缓存此页面。
  • accepts:内容类型发送请求头(Content-Type),用于通知服务器该请求需要接收何种类型的返回结果。
  • error:函数类型,请求失败时调用此函数。有以下三个参数:jqXHR (在 jQuery 1.4.x前为XMLHttpRequest) 对象、描述发生错误类型的一个字符串 和 捕获的异常对象。如果发生了错误,错误信息(第二个参数)除了得到null之外,还可能是"timeout""error""abort" ,和 "parsererror"。 当一个HTTP错误发生时,errorThrown 接收HTTP状态的文本部分,比如: “Not Found”(没有找到) 或者 “Internal Server Error.”(服务器内部错误)。
  • jsonp:字符串类型,在一个jsonp请求中重写回调函数的名字。这个值用来替代在”callback=?“这种GETPOST请求中URL参数里的”callback“部分,比如{jsonp:'onJsonPLoad'}会导致将”onJsonPLoad=?"传给服务器。
  • jsonpCallback:为jsonp请求指定一个回调函数名。这个值将用来取代jQuery自动生成的随机函数名。这主要用来让jQuery生成一个独特的函数名,这样管理请求更容易,也能方便地提供回调函数和错误处理。
  • mimeType:一个mime类型用来覆盖XHR的 MIME类型。
  • success:请求成功后的回调函数。这个函数传递3个参数:从服务器返回的数据,并根据dataType参数进行处理后的数据,一个描述状态的字符串;还有 jqXHR(在jQuery 1.4.x前为XMLHttpRequest) 对象 。
  • timeout:设置请求超时时间(毫秒)。此设置将覆盖$.ajaxSetup() 里的全局设置。
  • type:请求方式 (“POST” 或 “GET“), 默认为 “GET“。

Axio请求

Axio是一个基于Promise的网络请求库,同时适配于浏览器端和服务端(nodejs)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests

图 1.2 axios

安装

$ npm install axios # 或者使用yarn 
$ yarn add axios

通过CDN加载:

<!--jsDelivr CDN -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

<!--unpkg CDN -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

示例

axios.request({
      url:'/article/home/index',
      method:'get',
      baseURL:'http://test.mediastack.cn/'
}).then(
     res => {
     console.log("get res:",res);
     var str=JSON.stringify(res);
     document.getElementById("content").innerHTML = str;
},error => {
     console.log("get request failed:",error);
     document.getElementById("content").innerHTML = error;
  }
);

具体使用方式,可参考React应用中封装axios

fetch请求

fetch API提供了一个标准的js接口,优化http请求和响应的操作方式,fetch提供了一种极简单的方式来进行异步跨域获取资源,和axios类似的是其依托Promise,通过返回Promise对象实现了请求的异步。如下为fetch常见使用示例:

        var url = "https://mock.yonyoucloud.com/mock/16388/test/cities";
        fetch(url, {
            method: "GET",
            mode: 'cors',
            cache: 'default'
        }).then(function(response){
            return response.json(); //响应内容json化
        }).then(function(res){
            console.log(res);  //打印响应值
        })

如上,配置参数支持如下:

  • method: 请求方法,如 GET、POST、PUT、DELETE。默认GET
  • headers: 请求头信息,形式为 Headers 的对象或包含 ByteString 值的对象字面量。比如:headers : { 'Content-Type' : 'application/json' },可以设置所有的header信息。
  • body: 请求的 body 信息:可能是一个 Blob、BufferSource、FormData、URLSearchParams 或者 USVString 对象。注意 GET HEAD 方法的请求不能包含 body 信息。
  • mode: 请求的模式,如 默认cors跨域、 no-cors不跨域 或者 same-origin
  • credentials: 请求的 credentials,如 默认omit不携带cookiesame-origin同源携带cookie 或者 include跨域携带cookie。但在Chrome,Chrome 47 之前的版本默认值为 same-origin ,自Chrome 47起,默认值为include
  • cache: 请求的 cache 模式: default、 no-store不缓存、 reload 、 no-cache有缓存但仍需要去服务器获取资源 、 force-cache 或者 only-if-cached
  • redirect: 可用的 redirect 模式: follow (自动重定向), error (如果产生重定向将自动终止并且抛出一个错误), 或者 manual (手动处理重定向). 在Chrome中默认使用followChrome 47之前的默认值是manual)。
  • referrer: 一个 USVString 可以是 no-referrer、client或一个 URL。默认是 client
  • referrerPolicy: 指定了HTTP头部referer字段的值。可能为以下值之一:
    • no-referrer
    • no-referrer-when-downgrade
    • origin
    • origin-when-cross-origin
    • unsafe-url

XHR原生请求

xhr请求是浏览器端网络请求的基础,我们现在常用的所有网络请求框架都是基于xhr实现的二度封装,接下来,我们来了解下不用网络框架如何使用xhr实现前端的网络请求。代码示例如下:

// 创建 XHR 对象
const xhr = new XMLHttpRequest();
// 监听 onreadystatechange 事件
xhr.onreadystatechange = () => {
     if (xhr.status == 200 && xhr.readyState == 4) {
         console.log('请求成功', xhr.responseText);
     }
};
// 调用 open 函数
xhr.open('POST', 'http://localhost:8080/login', true);

// post 请求时,设置请求头 Content-Type
// 用于指定请求参数,以什么样的格式发送给后端,告诉服务器,我前端发送的请求参数是什么类型的。
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

// 调用 send 函数
xhr.send('a=100&b=200');

如上,通过onreadystatechange监听的回调函数获取返回数据。

websocket请求

websocket网络请求与上述其他网络请求方式不同,上述其他请求基本都是基于http协议实现的单次请求,而websocket是通过socket实现的前后端的长连接网络请求,一般适用于实时性要求高的功能需求,如我们常见的在线聊天室、实时推送等。

let ws = new WebSocket(url)
ws.onopen = function () {
  // 指定连接成功后的回调函数
}         
ws.onclose = function () {
  // 指定连接关闭后的回调函数
}         
ws.onmessage = function () {
  // 指定收到服务器数据后的回调函数
}         
ws.onerror = function () {
  // 指定报错时的回调函数
}

ws.send(message)  向服务器发送数据
ws.close()  关闭当前连接

有关websocket更加详细的介绍可参考:前端开发之websocket

Vue2的patch流程和diff算法

胖蔡阅读(364)

Vue通过虚拟DOM实现对页面渲染的监听更新,降低对真实DOM的操作。而patch则是Vue虚拟DOM实现的基石,它能快速实现虚拟DOM的对比更新,并最终将vnode渲染成真实DOM。整个patch的过程就是:创建节点、删除节点和修改节点的过程。

创建节点

当因状态改变而新增的节点在DOM中并不存在时,我们需要创建一个节点并插入到DOM中。即当oldVnode不存在而vnode存在时,就需要使用vnode生成真实的DOM元素并将其插入到视图当中。

1、首次渲染时,DOM中不存在任何节点,即oldVnode是不存在的,我们需要使用vnode创建一个新DOM节点并渲染视图。

2、当vnode oldVnode完全不是同一个节点时,即oldVnode是一个被废弃的节点,vnode是一个全新的节点,此时,我们需要使用vnode创建一个新DOM节点,用它去替换oldVnode所对应的真实DOM节点。

vnode是有类型的,所以当我们在创建节点时,最重要的是根据vnode的类型来创建出相同类型的DOM元素。事实上,只有三种类型的节点会被创建并插入到DOM中:元素节点注释节点文本节点

删除节点

因为渲染视图时,需以vnode为标准,所以vnode中不存在的节点都属于被废弃的节点,需要从DOM中删除。当vnode oldVnode完全不是同一个节点时,在DOM中需要使用vnode创建新节点替换oldVnode所对应的旧节点,而替换的过程就是将新创建的DOM节点插入到旧节点的旁边,然后再将旧节点删除。

在要删除节点的父元素上调用removeChild方法即可。

更新节点

无论是新增节点,还是删除节点,他们之间都有一个共同点,那就是两个虚拟节点是完全不同的。而当新旧两个节点是相同的节点时,我们需要对这两个节点进行比较细致的比对,然后对oldVnode在视图中所对应的真实节点进行更新。

更新节点需要对以下3种情况进行判断并分别处理:

  • 如果VNodeoldVNode均为静态节点:无需处理
  • 如果VNode是文本节点:如果VNode是文本节点即表示这个节点内只包含纯文本,那么只需看oldVNode是否也是文本节点,如果是,那就比较两个文本是否不同,如果不同则把oldVNode里的文本改成跟VNode的文本一样。如果oldVNode不是文本节点,那么不论它是什么,直接调用setTextNode方法把它改成文本节点,并且文本内容跟VNode相同。
  • 如果VNode是元素节点:如果VNode是元素节点,则又细分两种情况
    • 该节点不包含子节点
    • 该节点包含子节点

patch流程

1、当oldVnode不存在时,直接使用vnode渲染视图;

2、当oldVnodevnode都存在但并不是同一个节点时,使用vnode创建的DOM元素替换旧的DOM元素;

3、当oldVnodevnode是同一个节点时,使用更详细的对比操作对真实的DOM节点进行更新。

patch过程中最关键的是运营Vue-Diff算法,完成新旧vnode的精细化对比。通过Diff算法,我们可以计算出虚拟DOM中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面,从而提升性能。

原始diff算法

原始diff算法就是,两个虚拟DOM树,进行不分层级的逐一比对,也就是说,一个虚拟DOM树,从根节点到以后分支的每一个节点,都要单独拿出来跟新生成的节点做比较,这就是最原始的diff算法。

这个diff算法的时间复杂度表面上看是(n ^2),因为单独一个个的去跟另外的n个相比较,肯定是n ^2次就比较结束了,但是实际上不是的,比较完之后还要计算如何在最优的地方放置最佳的节点,所以就是O(n ^3)了。


虽然原始的Diff算法从功能上解决了先对比再处理实际DOM的需求,但是实际上我们的流程变得更加的复杂和笨拙。

优化Diff算法

优化Diff算法只比较同一层级 ,不做跨级比较。因为在实际的web展示中,非同级的节点移动是非常少的,所以可以选择做同级比较。

所谓同级比较,即只比较同层的节点,不同层不做比较。不同层的只需要删除原节点,并且新建插入更新节点。

Vue-Diff算法,采用的就是优化过的Diff算法,同层比较,不会跨级,且其比较是从两侧向中间进行的,这种方式相对于从左到右依次比对的方式来说,更高效。

Vue-diff 策略

1、Tree Diff

Tree Diff是对树每一层进行遍历,找出不同。

2、Component Diff

Vue是基于组件构建的,对于组件间的比较采用的策略如下:

  • 如果是同一类型的组件,则按照原策略比较组件的虚拟 DOM 树,否则不需要比较。
  • 如果是不同类型的组件,则将该组件判断为dirty component,从而替换整个组件下的所有子节点。

3、Element Diff

在进行组件对比的时候,如果两个组件类型相同,则需要进行元素级别的对比,这就是Element DiffElement Diff时,提供了3种节点操作,分别为INSERT_MARKUP(插入),MOVE_EXISTING(移动),REMOVE_NODE(删除)。

  • INSERT_MARKUP:新的组件类型不在旧集合中,即全新的节点,需要对新节点进行插入操作。
  • MOVE_EXISTING:旧集合中有新组件类型,且element是可更新的类型,这时候就需要做移动操作,可以复用以前的DOM节点。
  • REMOVE_NODE:旧组件类型,在新集合里也有,但对应的element不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合里的,也需要执行删除操作。

Vue-diff过程

定义4个指针:OldStartIdx、OldEndIdx、NewStartIdx、NewEndIdx。比较的是两个指针对应的节点的虚拟DOM是否为同一个。

具体步骤如下:

  1. 比较OldStartIdxNewStartIdx
  2. 如果两个startIdx相同,则两个指针都会+1,也就是向后移一位,重新生成OldStartIdxNewStartIdx指针。
  3. 如果两个startIdx不一致,则比较两个endIdx
  4. 比较OldEndIdxNewEndIdx
  5. 如果两个endIdx一致,则两个endIdx都减1,也就是向前移一位,再执行步骤1。
  6. 如果两个startIdx和两个endIdx都不一致,则比较捺向的oldStartIdxNewEndIdx
  7. 如果oldStartIdxNewEndIdx一致,则把oldStartIdx指向的虚拟DOM里的真实DOM节点,挪到OldEndIdx位置之后,oldStartIdx加1向后移一位,newEndIdx减1向前移动一位。
  8. 如果竖向和捺向都不一致,则比较撇向oldEndIdxNewStartIdx
  9. 如果撇向一致,则把oldEndIdx指向的真实dom节点挪到oldStartIdx所在的真实dom前,同时oldEndIdx减1向前移动一位,newStartIdx加1向后移动一位。
  10. 如果竖向、捺向、撇向都不一致,则看有没有key。
  11. 如果有key,就能快速找到,并挪到oldStartIdx前。
  12. 如果没有key,就遍历oldStartIdxoldEndIdx之间的所有节点,寻找newStartIdx指向的节点是否存在于这些老的vdom中。如果有,就把它挪到oldStartIdx前;没有就在oldStartIdx之前创建一个节点,newEndIdx减1向前移动一位。这样比较下去,一直到newEndIdx<newStartIdx

newEndIdx<newStartIdx,这时new vnode生成完毕,然后将old vnode中多余的部分删掉即可,也就是oldStartIdxoldEndIdx指向的dom及中间的部分。


Vue2的变化侦测机制

胖蔡阅读(289)

Vue最引人所乐道的就是其数据双向绑定式模式,以数据驱动视图。通过数据的改变实现视图的实时更新的这一过程其实就是对数据的一个状态的追踪,那么Vue2是通过何种方式实现界面的实时动态变化的呢?本篇文章主要介绍Vue2的这种变化侦测机制的实现原理。

Object的变化侦测

由于JSObjectArray提供的方法机制不同,所以Vue针对ObjectArray,采用了两套不同的变化侦

测机制。本章,我们先来详细介绍一下Object的变化侦测。

Object变化如何侦测

Vue变化侦测机制的关键点在于观测数据变化,那么我们如何观测Object的变化呢?

Vue为我们定义了Observer类。通过Observer类,可以将一个正常的数据转换成可观测的数据。

例如:

let apple = new Observer({
'weight':'1斤',
'price':10
})

这样,apple的两个属性都变得可观测了。

那么, observer 类究竟是怎样的一个存在呢?我们来看看源码:

/**
* Observer类会通过递归的方式把一个对象的所有属性包括子属性都转化成可观测对象
*/
export class Observer {
constructor (value) {
this.value = value
// 给value新增一个__ob__属性,值为该value的Observer实例
// 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
def(value,'__ob__',this)
if (Array.isArray(value)) {
// 当value为数组时的逻辑
// ...
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}

看到这里,大家可能还是不知道Observer类究竟是如何监测数据变化,实现数据可观测呢?

我们再把上述代码中的defineReactive方法代码放出来,大家就明白了。

/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive (obj,key,val) {
// 如果只传了obj和key,那么val = obj[key]
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
if(val === newVal){
return
}
console.log(`${key}属性被修改了`);
val = newVal;
}
})
}

defineReactive方法用到了JS一个非常重要的原生方法Object.defineProperty。通过

Object.defineProperty() 方法,可以给数据对象定义一个属性(key),并把这个属性的读和写分别

使用 get() set() 进行拦截,每当该属性进行读或写操作的时候就会触发 get() set() ,从而使得

数据变化可以观测。

谁使用了数据成为依赖

现在,我们知道了数据什么时候发生变化,该去通知视图去更新了。但是,视图那么大,我们到底该通

知谁去更新?总不能一个数据变化了,把整个视图全部更新一遍吧。

理想的情况是:视图里谁用到了这个数据、谁依赖了这个数据,就更新谁!

Vue当然是这么做的。

那么,Vue究竟怎么用代码的形式,来描述这个“谁”呢?请看以下代码:

/**
* @param { vm } vue对象实例
* @param { expOrFn } 对象的属性
* @param { cb } 真正包含数据更新逻辑的函数
*/
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn) //parsePath方法把一个形如'data.a.b.c'的字符串路
径所表示的值,从真实的data对象中取出来
//初始化的时候触发数据属性的get方法
this.value = this.get()
}
// 触发数据属性的get方法,访问数据属性即可实现
get () {
// 把Watcher实例保存到Dep类的target属性上
Dep.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm)
Dep.target = null;
return value
}
// 当update被触发时,此时获取到的数据属性值时已经被修改后的新值
update () {
const oldValue = this.value
this.value = this.get()
// 触发传递给Watcher的更新数据的函数
this.cb.call(this.vm, this.value, oldValue)
}
}

从上述代码可以知道,在创建 Watcher 实例的过程中会自动的把自己添加到这个数据对应的依赖管理器

中,以后这个 Watcher 实例就代表这个依赖,当数据变化时,我们就通知 Watcher 实例,由 Watcher

实例再去通知真正的依赖。

依赖放在哪里集中管理

一个数据可能被多处使用,产生众多依赖,这些依赖如何管理?

简单的做法是,我们给每个数据都建一个依赖数组,谁依赖了这个数据,我们就把谁放入这个依赖数组

中,那么当这个数据发生变化的时候,我们就去它对应的依赖数组中,把每个依赖都通知一遍。

更好的做法是,我们为每一个数据都建立一个依赖管理器,把这个数据所有的依赖都管理起来。所以,

vue创建了Dep类。

/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 删除一个依赖
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
/*依赖收集,当存在Dep.target的时候添加依赖*/
depend () {
if (Dep.target) {
Dep.target.addDep(this) // *Watcher里的addDep方法,添加一个依赖关系到Deps集合中
*/
}
}
/*通知所有依赖更新*/
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() //Watcher里面的调度者接口,当依赖发生改变的时候进行回调
}
}
}
/*依赖收集完需要将Dep.target设为null,防止后面重复添加依赖。*/
Dep.target = null
const targetStack = []
/*将watcher实例设置给Dep.target,用以依赖收集。同时将该实例存入target栈中*/
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
/*将观察者实例从target栈中取出并设置给Dep.target*/
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}

Object依赖何时收集何时更新

数据变化知道了,依赖是谁也知道了,依赖放哪也明确了,那么何时才能收集依赖,又是何时才能去通

知依赖进行更新:

function defineReactive (obj,key,val) {
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
const dep = new Dep() //实例化一个依赖管理器,生成一个依赖管理数组dep
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
dep.depend() // 在getter中收集依赖
return val;
},
set(newVal){
if(val === newVal){
return
}
val = newVal;
dep.notify() // 在setter中通知依赖更新
}
})
}

可观测的数据被获取时会触发 getter 属性,所以我们就可以在 getter 中收集这个依赖。同样,当这个

数据变化时会触发 setter 属性,那么我们就可以在 setter 中通知依赖更新。

关于Object侦测的问题

前面介绍了Object类型数据的变化侦测原理,了解了数据的变化是通过getter/setter来追踪的。

但是,也正是由于这种追踪方式,有些语法中即便是数据发生了变化,Vue也追踪不到。例如:

data() {
return {
obj: {}
}
}
methods: {
addKey() { //在obj上面新增name属性
this.obj.name = 'apple'
}
deleteKey() {//在obj上面删除name属性
delete this.obj.name
}
}

在上面代码中,无论是为obj新增name属性,还是删除name属性,Vue都无法侦测到。

Vue是通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但

getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性,所以才会导致上述问题。

为了解决这一问题,Vue增加了两个全局API:Vue.set和Vue.delete。关于这两个API的实现原理,我们

后续再介绍。

总结

Object通过Observer类将其所有数据包括子数据都转换成getter/setter的形式来追踪变化。

我们在getter中收集依赖,在setter中通知依赖。

收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依

赖发送消息等。

所谓的依赖,其实就是Watcher。只有Watcher触发的getter来会收集依赖,哪个Watcher触发了

getter,就把哪个Watcher收集到Dep中,当数据发生变化时,会循环依赖列表,把所有的watcher都通

知一遍。

数据与Observer、Dep、watcher之间的运转流程如下:

  • 数据 通过 Observer 转换成可侦测的对象。
  • 当外界通过 Watcher 读取数据时,会将 Watcher 添加到 Dep 中
  • 当数据发生变化时,则会向 Dep 中的依赖即 Watcher 发送通知。
  • Watcher 接收到通知后,会向外界发送通知。外界接收到通知后进行相应的更新。

Array的变化侦测

我们知道,Object的变化是靠setter来追踪的,只要一个数据发生变化,就一定会触发setter。那Array

是不是这样呢?

我们先来看下面这个例子:

this.list.push(1)

该例子中,我们使用了push方法向list中新增了数字1,改变了list数组,但并没有触发setter。

也就是说,我们可以通过Array原型上的方法来改变数组的内容,而无需触发setter,所以Object那种通

过getter/setter来实现侦测的方式用在Array身上就行不通了。

为此,Vue中,专门创建了Array变化侦测机制。

虽然Object和Array的变化侦测机制不同,但是在讲述Object变化侦测机制中提到的Observer、Dep、

watcher三个类及其概念,同样适用于Array。

下面我们正式开始Array的变化侦测的介绍。

Array变化如何侦测

前面的例子使用push来改变数组的内容,那么我们只要能在用户使用push操作数组的时候得到通知,

那么就能追踪数据的变化了。来看看下面这段代码:

let arr = [1,2,3]
arr.push(4)
Array.prototype.newPush = function(val){
console.log('arr被修改了')
this.push(val)
}
arr.newPush(4)

我们针对数组的原生push方法定义了一个新的newPush方法,这个newPush方法内部调用了原生push

方法,这样既能保证新的newPush方法跟原生push方法具有相同的功能,而且我们还能得知数组变化。

创建拦截器

基于上述思想,Vue中创建了一个数组方法拦截器,重写了操作数组的方法,并把它拦截在数组实例与

Array.prototype之间。当数组实例使用操作数组方法时,其实使用的是拦截器中重写的方法,而不再使

用Array.prototype上的原生方法。

具体实现代码如下:

const arrayProto = Array.prototype
// 创建一个对象作为拦截器
export const arrayMethods = Object.create(arrayProto)
// 改变数组自身内容的7个方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method] // 缓存原生方法
Object.defineProperty(arrayMethods, method, {
enumerable: false,
configurable: true,
writable: true,
value:function mutator(...args){
const result = original.apply(this, args)
return result
}
})
})

在上面的代码中,我们创建了变量arrayMethods,它继承自Array.prototype,具备其所有功能。

接下来,在arrayMethods上使用Object.defineProperty方法,将那些可以 改变数组自身内容的方法

(push,pop,shift,unshift,splice, sort, reverse)进行封装。

所以,当使用push方法的时候,其实调用的是arrayMethods.push,执行的是mutator函数,而

mutator是可控的,我们可以在里面做一些其他的事情,诸如发送变化通知等。

挂载拦截器

有了拦截器之后,想要它生效,就需要使用它去覆盖Array.prototype。但是我们不能直接覆盖,因为这

样会污染全局的Array,所以我们可以这样操作:

const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export class Observer {
constructor (value) {
this.value = value
if (Array.isArray(value)) {
//挂载关键代码
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value)
}
}
}
function protoAugment (target, src: Object, keys: any) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}

上面代码中首先判断了浏览器是否支持 __proto__ ,如果支持,则调用protoAugment函数覆盖value

原型功能;如果不支持,则调用copyAugment函数把拦截器中重写的7个方法循环加入到value上。这

样,拦截器就可以生效了。

拦截器生效以后,当数组数据再发生变化时,我们就可以在拦截器中通知变化了。

Array依赖在哪收集

我们创建了拦截器,让我们具备了当数组内容发生变化时得到通知的能力。但是变化了通知谁呢?当然

是用到了Array型数据的那些依赖。那么这些依赖,我们该如何收集呢?

在这之前,我们先简单回顾一下Object的依赖的在哪收集的。

Object的依赖是在Object.defineProperty中的getter里收集的,每个key都会有一个对应的Dep列表来存

储依赖。

那么,数组在哪里收集依赖呢?其实,数组也是在getter中收集依赖的。为什么这么说呢?我们来看看

下面这个例子。

data(){
return {
list:[1,2,3]
}
}

想想看,list这个数据始终都存在于一个object数据对象中,而且我们也说了,谁用到了数据谁就是依

赖,那么要用到list这个数据,是不是得先从object数据对象中获取一下list数据,而从object数据对象中

获取list数据自然就会触发list的getter,所以我们就可以在getter中收集依赖。

总结一句话就是:Array型数据还是在getter中收集依赖。

Array依赖列表放在哪

知道在哪收集依赖了,那么将收集来的的依赖列表放在哪呢?Vue中,把Array的依赖列表放在Observer

中。

export class Observer {
constructor (value) {
this.value = value
this.dep = new Dep() // 实例化一个依赖管理器,用来收集数组依赖
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value)
}
}
}

从上面的介绍中,我们知道,Array在getter中收集依赖,在拦截器中通知依赖更新。所以这个依赖保存

的位置就很关键,它必须在getter和拦截器中都可以访问到,而Observer实例正好在getter中、拦截器

中都能访问到。

Array依赖如何收集

把Dep实例保存在Observer的属性上之后,我们可以在getter中像下面这样访问并收集依赖。

function defineReactive (obj,key,val) {
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
//收集依赖
if (childOb) {
childOb.dep.depend()
}
return val;
},
set(newVal){
}
})
}
/**
* 尝试为value创建一个0bserver实例,如果创建成功,直接返回新创建的Observer实例。
* 如果 Value 已经存在一个Observer实例,则直接返回它
*/
export function observe (value, asRootData){
if (!isObject(value) || value instanceof VNode) {
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}

在上面的代码中,我们通过 observe函数,创建一个Observer实例,如果value已经是响应式数据,则

不需要再次创建Observer实例,直接返回已经创建的Observer实例即可,避免了重复侦测value变化的

问题。

Observer实例childOb创建后,我们就可以访问其dep属性 ,调用该属性上的depend()方法,收集依赖

了。

如何通知Array依赖更新

到现在为止,依赖已经收集好了,并且也已经存放好了,那么我们该如何通知依赖呢?

因为我们是在拦截器中获知数组变化的,所以我们应该在拦截器里通知依赖,而要想通知依赖,首先要

能访问到依赖。

export class Observer实例。 {
constructor (value) {
this.value = value
this.dep = new Dep()
def(value,'__ob__',this) //通过def工具函数,在value中新增一个__ob__属性,这个属性的
值就是当前的Observer实例。
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value)
}
}
}

上述代码中,我们通过def工具函数,在value中新增一个 __ob__ 属性,这个属性的值就是当前的

Observer实例。然后我们就可以在拦截器中,通过 __ob__ 属性拿到Observer实例,然后就可以拿到

__ob__ 的dep了,从而调用dep身上的notify()方法通知依赖更新。具体代码如下:

methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__ //拿到Observer实例
ob.dep.notify()//拿到Observer实例的dep属性,调用notify()方法通知依赖更新
return result
})
})

Array的深度监测

我们上面说的侦测数组的变化,指的是数组自身的变化,比如是否新增一个元素,是否删除一个元素

等。

实际上,数组的子数据的变化也要侦测。比如数组中Object身上某个属性的值发生了变化也要发送通

知。再比如,使用了push往数组中新增了元素,这个新增元素的变化也要侦测。

1、侦测数组中元素的变化

如何侦测数组中子数据的变化,我们来看看下面这段代码:

export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value) // 将数组中的所有元素都转化为可被侦测的响应式
} else {
this.walk(value)
}
}
// 侦测数组中的每一项
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

在上面代码中,我们调用observeArray方法,循环Array中的每一项,执行observe函数,将数组中的每

个元素都执行一遍new Observer,即可将这个数组的所有子数据转换成响应式的。

2、侦测新增元素的变化

对于数组中已有的元素我们已经可以将其全部转化成可侦测的响应式数据了,但是如果向数组里新增一

个元素的话,我们也需要将新增的这个元素转化成可侦测的响应式数据。

如若达到此目的,我们需要拿到新增的这个元素,然后调用observeArray函数将其转化即可。

我们知道,可以向数组内新增元素的方法有3个,分别是:push、unshift、splice。我们只需对这3中方

法分别处理,拿到新增的元素,再将其转化即可。

methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args // 如果是push或unshift方法,那么传入参数就是新增的元素
break
case 'splice':
inserted = args.slice(2) // 如果是splice方法,那么传入参数列表中下标为2的就是新
增的元素
break
}
if (inserted) ob.observeArray(inserted) // 调用observeArray函数将新增的元素转化成
响应式
// notify change
ob.dep.notify()
return result
})
})

在上述代码中,我们从 this.__ob__ 上拿到Observer实例后,如果有新增元素,则使用

ob.observeArray来侦测这些新元素的变化。

关于Array侦测的问题

从前面的介绍,我们知道,Vue2中对Array的变化侦测是通过拦截原型中操作数组的方法的方式实现

的,但是,其实我们是可以不通过使用数组原型方法来改变数组的。例如:

this.list[0] = 2 // 改变数组的第一个值
this.list.length = 0 // 清空数组

如果使用上述方式改变数组,Vue是侦测不到的。

为了解决这一问题,与Object一样,同样需用到了Vue.set和Vue.delete这两个API。

总结

Array追踪变化的方式和Object不一样。因为它是通过方法来改变内容的,所以我们通过创建拦截器去覆

盖数组原型的方式来追踪变化。

Array收集依赖的方式和Object一样,都是在getter中收集。但是由于使用依赖的位置不同,数组要在拦

截器中向依赖发送消息,所以依赖不能像Object那样保存在defineReactive中,而是把依赖保存在了

Observer实例上。

除了侦测数组自身的变化外,数组中元素发生的变化也要侦测。

除了侦测已有数据外,当使用push等方法向数组中新增数据时,新增的数据也要进行变化侦测。

JS中如何理解执行上下文?

胖蔡阅读(345)

执行上下文是JS中一个比较重要的概念,当前函数、变量所处的执行上下文直接决定当前变量、对象可访问哪些数据。一般地,根据执行区域可以将执行上下文分为两种类型:

  • 全局执行上下文
  • 函数执行上下文
执行上下文

全局执行上下文

一般地,全局执行上下文是最外层的执行上下文,总在执行栈底部,我们可以理解为window对象作为一个全局执行上下文的对象。因此,所有通过var定义的全局变量和函数都可以通过window访问对应的属性和方法。全局执行上下文在一个页面打开自动的时候创建,直到这个页面关闭后全局执行上下文会被回收。

函数执行上下文

如上,在JS中执行是按顺序执行的,JS启动的时候会创建一个执行上下文的一个栈结构,并同时创建一个全局执行上下文压在栈底,然后按顺序执行。这时候,若是遇到需要执行一个函数的话,JS就会创建一个对应的函数执行上下文入栈,并在该函数执行结束后,将该函数执行上下文出栈。

var a=1;//1.进入全局执行上下文环境
function outter(){
  var b=2;
  function inner(){
    var c=3;
    console.log(a+b+c);
  }
  inner();//3.进入inner函数上下文环境
}
outter();//2.进入outter函数上下文环境

上述代码,是比较常见介绍执行上下文的代码。如上所示,开始执行JS时,会创建全局执行上下文,然后运行到outter()函数将触发创建函数执行上下文环境,在outter内部又会创建一个子函数执行上下文inner(),我们简单的梳理下上下文创建、销毁的流程:

  1. 打开页面,运行JS代码时创建全局执行上下文环境
  2. 调用outter()函数,创建outter()函数执行上下文环境
  3. 运行执行outter函数,调用内部函数inner()函数,创建inner()函数执行上下文环境
  4. inner()函数执行完毕,销毁inner函数执行上下文环境
  5. outter()函数执行完毕,销毁outter函数执行上下文环境
  6. 关闭当前页面,销毁全局执行上下文环境

执行上下文组成

执行上下文环境一般的由变量环境和词法环境组成。变量环境可以理解为我们的全局变量、全局函数,或者是函数执行上下文里的函数变量部分。词法环境主要就是js代码执行的调用链即代码作用域。

JavaScript中的正则表达式基础使用介绍

胖蔡阅读(297)

什么是正则表达式?它有什么作用?

又称规则表达式,(Regular Expression,在代码中常简写为regex、regexp或RE),是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为”元字符”),是计算机科学的一个概念。

正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串,通常被用来检索、替换那些符合某个模式(规则)的文本。

JS 正则表达式

如何使用正则表达式

定义表达式

  • 普通方式

let reg=/表达式/修饰符(可选)

修饰符
g:代表可以进行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。
i:代表不区分大小写匹配。
m:代表可以进行多行匹配。
例如
var reg=/a*b/;
var reg=/abc+f/g;
  • 构造函数方式

let reg=new RegExp(“表达式”,”附加参数”);

例如

var reg=new RegExp(“a*b”); 
var reg=new RegExp(“abc+f”,”g”);
  • 两者区别

1、普通方式中的表达式必须是一个常量字符串,
2、构造函数中的表达式可以是常量字符串,也可以是一个js变量,
3、例如根据用户的输入来作为表达式参数等等:var reg=new RegExp(动态变量,”g”);

表达式操作

1、test(str) 判断字符串str是否匹配表达式,返回一个布尔值。

let regx=/123/; 
let flag=regx.test('123sdsd'); // 匹配123sdsd中是否包含123, 结果 flag 返回 true

2、exec(str) 返回str中与表达式相匹配的第一个字符串,而且以数组的形式表现

var str="the name 123 and 456";
var reg=/\d/g; reg.lastIndex=15; // 在全局模式下可以使用lastIndex属性设置在字符串中查找指定字符时开始的位置。 
console.log(reg.exec(str)); ['4', index: 17, input: 'the name 123 and 456', groups: undefined] 

如果有多个合适的匹配,则第一次执行exec返回一个第一个匹配,此时继续执行exec,则依次返回第二个第三个匹配。 例如:

var regx=/user\d/g; 
var rs=regx.exec('ddduser1dsfuser2dd'); 
var rs1=regx.exec('ddduser1dsfuser2dd'); 
console.log(rs) ['user1', index: 3, input: 'ddduser1dsfuser2dd', groups: undefined] console.log(rs1)['user2', index: 11, input: 'ddduser1dsfuser2dd', groups: undefined] 
// 当然注意regx中的g参数是必须的,否则无论exec执行多少次,都返回第一个匹配。后面还有相关内容涉及到对此想象的解释

3、match(expr) 返回与expr相匹配的一个字符串数组,如果没有加参数g,则返回第一个匹配,加入参数g则返回所有的匹配

var regx=/user\d/g; 
var str='user13userddduser345'; 
var rs=str.match(regx); 
console.log(rs) ['user1', 'user3']

4、search(expr),返回字符串中与expr相匹配的第一个匹配的index值。

var regx=/user\d/g; 
var str='user13userddduser345'; 
var rs=str.search(regx); 
console.log(rs) 、// 0

5、replace(expr,str),将字符串中匹配expr的部分替换为str。另外在replace方法 中,str中可以含有一种变量符号$,格式为$n,代表匹配中被记住的第n的匹配字符串(注意小括号可以记忆匹配)

示例一:

var regx=/user\d/g;
var str='user13userddduser345';
var rs=str.replace(regx,'00');
console.log(rs) 003userddd0045

示例二:

var regx=/u(se)r\d/g; 
var str=“user13userddduser345”; 
var rs=str.replace(regx,”$1”); 
console.log(rs) // se3userdddse45 

对于replace(expr,str)方法还要特别注意一点,如果expr是一个表达式对象则会进行全局替换(此时表达式必须附加参数g,否则也只是替换第一个匹配),如果expr是一个字符串对象,则只会替换第一个匹配的部分,例如:

var regx='user'; 
var str='user13userddduser345'; 
var rs=str.replace(regx,'00'); 
console.log(rs)

6、split(expr),将字符串以匹配expr的部分做分割,返回一个数组,而且表达式是否附加参数g都没有关系,结果是一样的。

var regx=/user\d/g; 
var str='user13userddduser345'; 
var rs=str.split(regx); 
console.log(rs) // ['', '3userddd', '45']

正则表达式模式

方括号查询范围内符合的内容

正则表达式模式说明
[abc]查找方括号之间的任何字符。
[0-9]查找任何从 0 至 9 的数字。
(xy)
元字符是拥有特殊含义的字符
元字符说明
—-—-
\d查找数字。
\D查找非数字字符
\s查找空白字符。
\S查找非空白字符
\b匹配单词边界
\B匹配非单词边界
量词:
元字符说明
—-—-
^匹配开头,在多行检测中,会匹配一行的开头
$匹配结尾,在多行检测中,会匹配一行的结尾
n+匹配任何包含至少一个 n 的字符串。
n*匹配任何包含零个或多个 n 的字符串。
n?匹配任何包含零个或一个 n 的字符串。
n{x}匹配包含 x 个 n 的序列的字符串
n{x,y}匹配包含最少 x 个、最多 y 个 n 的序列的字符串
n{x,}匹配包含至少 x 个 n 的序列的字符串
正则说明

示例如下:

var s = "how are you";
var r = /\w+$/;
var a = s.match(r); //返回数组["you"]
var r = /^\w+/;var a = s.match(r); //返回数组["how"]
var r = /\w+/g;var a = s.match(r); //返回数组["how","are","you"]

常用一些表达式

1、手机校验

//手机号校验 
const phoneReg = /^[1][3,4,5,6,7,8,9][0-9]{9}$/  
//身份证的校验 
const sfzReg = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
//邮箱的校验
const emailReg = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/
//URL的校验
const urlReg = /^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/
//IPv4的校验
const ipv4Reg = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
//16进制颜色的校验
const color16Reg = /^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/
//日期 YYYY-MM-DD
const dateReg = /^\d{4}(\-)\d{1,2}\d{1,2}$/
//日期 YYYY-MM-DD hh:mm:ss
const dateReg = /^(\d{1,4})(-|\/)(\d{1,2})(\d{1,2}) (\d{1,2}):(\d{1,2}):(\d{1,2})$/
//整数的校验
const intReg = /^[-+]?\d*$/
//小数的校验
const floatReg = /^[-\+]?\d+(\.\d+)?$/
//保留n位小数
function checkFloat(n) {
  return new RegExp(`^([1-9]+[\d]*(.[0-9]{1,${n}})?)$`)
}
// 保留2位小数
const floatReg = checkFloat(2)

const floatNum1 = 1234.5
console.log(floatReg.test(floatNum1)) // true
//邮政编号的校验
const postalNoReg = /^\d{6}$/
//QQ号的校验5-11位数字
const qqReg = /^[1-9][0-9]{4,10}$/
//微信号的校验 说明:6至20位,以字母开头,字母,数字,减号,下划线
const wxReg = /^[a-zA-Z]([-_a-zA-Z0-9]{5,19})+$/
//车牌号的校验
const carNoReg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/
//只含字母的字符串
const letterReg = /^[a-zA-Z]+$/
//包含中文的字符串
const cnReg = /[\u4E00-\u9FA5]/
//密码强度的校验  说明:密码中必须包含字母、数字、特称字符,至少8个字符,最多30个字符
const passwordReg = /(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9]).{8,30}/
//字符串长度n的校验
function checkStrLength(n) {
  return new RegExp(`^.{${n}}$`)
}
// 校验长度为3的字符串
const lengthReg = checkStrLength(3)

const str1 = 'hhh'
console.log(lengthReg.test(str1)) // true
//文件拓展名的校验
function checkFileName (arr) {
  arr = arr.map(name => `.${name}`).join('|')
  return new RegExp(`(${arr})$`)
}
const filenameReg = checkFileName(['jpg', 'png', 'txt'])
//匹配img和src
const imgReg = /<img.*?src=[\"|\']?(.*?)[\"|\']?\s.*?>/ig

const htmlStr = '<div></div><img src="sunshine.png" /><img src="sunshine111.png" />'

console.log(imgReg.exec(htmlStr))

2、邮箱校验

var reg = new RegExp("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$");
     //校验
     if (value==null){
         alert("邮箱地址不能为空");
         return false;
     }
     if(!reg.test(value)){
         alert("请输入有效的邮箱地址");
         return false;
     }
     return  true;

3、身份证号校验

const validateIdent = {
    aIdentityCode_City: { // 城市代码列表  
        11: "北京", 12: "天津", 13: "河北", 14: "山西", 15: "内蒙古", 21: "辽宁", 22: "吉林",
        23: "黑龙江 ", 31: "上海", 32: "江苏", 33: "浙江", 34: "安徽", 35: "福建", 36: "江西",
        37: "山东", 41: "河南", 42: "湖北 ", 43: "湖南", 44: "广东", 45: "广西", 46: "海南",
        50: "重庆", 51: "四川", 52: "贵州", 53: "云南", 54: "西藏 ", 61: "陕西", 62: "甘肃",
        63: "青海", 64: "宁夏", 65: "新疆", 71: "台湾", 81: "香港", 82: "澳门", 91: "国外 "
    },
    IdentityCode_isCardNo(card) {//检查号码是否符合规范,包括长度,类型  
        var reg = /(^\d{15}$)|(^\d{17}(\d|X)$)/; //身份证号码为15位或者18位,15位时全为数字,18位前17位为数字,最后一位是校验位,可能为数字或字符X  
        if (reg.test(card) === false) {
            return false;
        }
        return true;
    },
    IdentityCode_checkProvince(card) { //取身份证前两位,校验省份    
        var province = card.substr(0, 2);
        if (validateIdent.aIdentityCode_City[province] == undefined) {
            return false;
        }
        return true;
    },
    IdentityCode_checkBirthday(card) { //检查生日是否正确,15位以'19'年份来进行补齐。  
        var len = card.length;
        //身份证15位时,次序为省(3位)市(3位)年(2位)月(2位)日(2位)校验位(3位),皆为数字    
        if (len == '15') {
            var re_fifteen = /^(\d{6})(\d{2})(\d{2})(\d{2})(\d{3})$/;
            var arr_data = card.match(re_fifteen); // 正则取号码内所含出年月日数据  
            var year = arr_data[2];
            var month = arr_data[3];
            var day = arr_data[4];
            var birthday = new Date('19' + year + '/' + month + '/' + day);
            return validateIdent.IdentityCode_verifyBirthday('19' + year, month, day, birthday);
        }
        //身份证18位时,次序为省(3位)市(3位)年(4位)月(2位)日(2位)校验位(4位),校验位末尾可能为X    
        if (len == '18') {
            var re_eighteen = /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$/;
            var arr_data = card.match(re_eighteen); // 正则取号码内所含出年月日数据  
            var year = arr_data[2];
            var month = arr_data[3];
            var day = arr_data[4];
            var birthday = new Date(year + '/' + month + '/' + day);
            return validateIdent.IdentityCode_verifyBirthday(year, month, day, birthday);
        }
        return false;
    },
    IdentityCode_verifyBirthday(year, month, day, birthday) {//校验日期 ,15位以'19'年份来进行补齐。
        var now = new Date();
        var now_year = now.getFullYear();
        //年月日是否合理    
        if (birthday.getFullYear() == year
            && (birthday.getMonth() + 1) == month
            && birthday.getDate() == day) {
            //判断年份的范围(3岁到150岁之间)    
            var time = now_year - year;
            if (time >= 3 && time <= 150) {
                return true;
            }
            return false;
        }
        return false;
    },
    IdentityCode_checkParity(card) { //校验位的检测  
        card = validateIdent.IdentityCode_changeFivteenToEighteen(card); // 15位转18位    
        var len = card.length;
        if (len == '18') {
            var arrInt = new Array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2);
            var arrCh = new Array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
            var cardTemp = 0, i, valnum;
            for (i = 0; i < 17; i++) {
                cardTemp += card.substr(i, 1) * arrInt[i];
            }
            valnum = arrCh[cardTemp % 11];
            if (valnum == card.substr(17, 1)) {
                return true;
            }
            return false;
        }
        return false;
    },
    IdentityCode_changeFivteenToEighteen(card) {  //15位转18位身份证号   
        if (card.length == '15') {
            var arrInt = new Array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2);
            var arrCh = new Array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
            var cardTemp = 0, i;
            card = card.substr(0, 6) + '19' + card.substr(6, card.length - 6);
            for (i = 0; i < 17; i++) {
                cardTemp += card.substr(i, 1) * arrInt[i];
            }
            card += arrCh[cardTemp % 11];
            return card;
        }
        return card;
    },
    IdentityCodeValid(card) {//   身份证号码检验主入口 
        let pass = true;
        let sex = ''
        //是否为空    
        if (pass && card === '')
            pass = false;
        //校验长度,类型    
        if (pass && validateIdent.IdentityCode_isCardNo(card) === false)
            pass = false;
        //检查省份    
        if (pass && validateIdent.IdentityCode_checkProvince(card) === false)
            pass = false;
        //校验生日    
        if (pass && validateIdent.IdentityCode_checkBirthday(card) === false)
            pass = false;
        //检验位的检测    
        if (pass && validateIdent.IdentityCode_checkParity(card) === false)
            pass = false;
        if (pass) {
            var iCard = validateIdent.IdentityCode_changeFivteenToEighteen(card);
            if (parseInt(iCard.charAt(16)) % 2 == 0) {
                sex = "0"; // 女生  
            } else {
                sex = "1"; // 男生  
            }
            return true
        } else {
            return false
        }
    }
}
 
 
 
 
export default validateIdent.IdentityCodeValid   //导出

一款轻量级处理cookie的javascript库推荐,无依赖

胖蔡阅读(498)

Js-cookie是一款用于处理 cookie 的简单、轻量级 JavaScript API。接下来我们就来了解下js-cookie,并学会快速的使用它。

  • 支持所有浏览器
  • 接受任何类型编码字符
  • 大量的测试用例
  • 无依赖性,不需要依赖其他任何包
  • 支持 ES 模块
  • 支持 AMD/CommonJS
  • RFC 6265 兼容
  • 启用自定义编码/解码
  • < 800 字节压缩!

安装

1、使用NPM安装

js-cookie 支持npm下载安装,安装代码如下:

$ npm i js-cookie

npm 包有一个 module 字段指向库的 ES 模块变体,主要是为 ES 模块感知捆绑器提供支持,而它的 browser 字段指向 UMD 模块以完全向后兼容。

2、直接下载引用

小伙伴可以直接点击访问js-cookie库的github仓库进行下载。文章的最后我也会提供百度网盘的下载链接给大家,以供哪些网速不是太好的同学使用。

需要注意的是,从版本 3 开始,发行版包含该库的两个变体,一个 ES 模块和一个 UMD 模块。注意不同的扩展名:.mjs 表示 ES 模块,而 .js 是 UMD 模块。如何在浏览器中加载 ES 模块的示例:

<script type="module" src="/path/to/js.cookie.mjs"></script>
<script type="module">
  import Cookies from '/path/to/js.cookie.mjs'

  Cookies.set('foo', 'bar')
</script>

并非所有浏览器都原生支持 ES 模块。出于这个原因,npm 包/发布提供了 ES UMD 模块变体,您可能希望将 ES 模块与 UMD 后备一起包含以解决此问题:

<script type="module" src="/path/to/js.cookie.mjs"></script>
<script nomodule defer src="/path/to/js.cookie.js"></script>

这里我们以延迟方式加载 nomodule 脚本,因为 ES 模块默认是延迟的。根据您使用库的方式,这可能不是绝对必要的。有关nomodule的使用,可以参考之前写的:你真的会用script吗?noscript又是什么?这篇文章的介绍。

3、使用CDN

官方问题提供了一个JSDELiVR的CDN服务地址。大家可以点击访问。可以根据需求选择配置CDN包含的文件,我这边给一个包含完整js-cookie的CDN使用代码:

<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.1/dist/js.cookie.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.1/index.min.js"></script>

ES Module

如何从另一个模块导入 ES 模块的示例:

import Cookies from 'js-cookie'

Cookies.set('foo', 'bar')

基本用法

  • 创建一个在整个网站上有效的 cookie
Cookies.set('name', 'value')
  • 创建一个从现在起 7 天后过期的 cookie,在整个站点中有效
Cookies.set('name', 'value', { expires: 7 })
  • 创建一个过期cookie,对当前页面的路径有效
Cookies.set('name', 'value', { expires: 7, path: '' })
  • 读取cookie
Cookies.get('name') // => 'value'
Cookies.get('nothing') // => undefined
  • 读取所有可见cookie
Cookies.get() // => { name: 'value' }

注意:无法通过传递其中一个 cookie 属性(在写入相关 cookie 时可能会或可能不会使用)来读取特定 cookie:

Cookies.get('foo', { domain: 'sub.example.com' }) // `domain` won't have any effect...!

名称为 foo 的 cookie 仅在 .get() 上可用,前提是它从调用代码的位置可见;阅读时域和/或路径属性将不起作用。

  • 删除cookie
Cookies.remove('name')
  • 删除一个对当前页面路径有效的cookie
Cookies.set('name', 'value', { path: '' })
Cookies.remove('name') // fail!
Cookies.remove('name', { path: '' }) // removed!

非常重要的一点,删除 cookie 并且您不依赖默认属性时,您必须传递用于设置 cookie 的完全相同的路径和域属性:

Cookies.remove('name', { path: '', domain: '.yourdomain.com' })


注意:删除不存在的 cookie 既不会引发任何异常,也不会返回任何值。

命名空间冲突

如果存在与命名空间 Cookie 冲突的任何危险,则 noConflict 方法将允许您定义一个新的命名空间并保留原来的命名空间。这在第三方站点上运行脚本时特别有用,例如作为小部件或 SDK 的一部分。

// Assign the js-cookie api to a different variable and restore the original "window.Cookies"
var Cookies2 = Cookies.noConflict()
Cookies2.set('name', 'value')

注意: .noConflict 方法在使用 AMD 或 CommonJS 时不是必需的,因此它不会在这些环境中公开。

编码问题

该项目符合 RFC 6265。 cookie-name 或 cookie-value 中不允许的所有特殊字符都使用百分比编码使用每个对应的 UTF-8 Hex 进行编码。
cookie-name 或 cookie-value 中唯一允许且仍在编码的字符是百分比 % 字符,它被转义以便将百分比输入解释为文字。
请注意,默认的编码/解码策略意味着只能在 js-cookie 读取/写入的 cookie 之间进行互操作。要覆盖默认的编码/解码策略,您需要使用转换器。

Cookie属性


Cookie 属性默认值可以通过 withAttributes() 创建 api 实例来全局设置,或者通过将普通对象作为最后一个参数传递来单独为每次调用 Cookies.set(…) 设置。每次调用属性会覆盖默认属性。

expires

定义何时删除 cookie。值必须是一个数字,它将被解释为从创建之日算起的天数或一个 Date 实例。如果省略,cookie 将成为会话 cookie。

要创建在不到一天内过期的 cookie,您可以查看 Wiki 上的常见问题解答。

  • 默认值:当用户关闭浏览器时,Cookie 被删除。
  • 示例
Cookies.set('name', 'value', { expires: 365 })
Cookies.get('name') // => 'value'
Cookies.remove('name')

path

一个字符串,指示 cookie 可见的路径。

  • 默认值:/
  • 示例
Cookies.set('name', 'value', { path: '' })
Cookies.get('name') // => 'value'
Cookies.remove('name', { path: '' })

关于 Internet Explorer 的注意事项:

由于底层 WinINET InternetGetCookie 实现中的一个模糊错误,如果使用包含文件名的路径属性设置 IE 的 document.cookie,它将不会返回 cookie。

 Internet Explorer Cookie Internals (FAQ)


这意味着不能使用 window.location.pathname 设置路径,以防此类路径名包含如下文件名:/check.html(或至少,无法正确读取此类 cookie)。

事实上,您绝不应该允许不受信任的输入来设置 cookie 属性,否则您可能会受到 XSS 攻击。

domain

一个字符串,指示 cookie 应该可见的有效域。该 cookie 也将对所有子域可见。

  • 默认值:Cookie 仅对创建 cookie 的页面的域或子域可见,Internet Explorer 除外(见下文)。
  • 示例:

假设在 enjoytoday.cn 上创建了一个 cookie

Cookies.set('name', 'value', { domain: 'cookie.enjoytoday.cn' })
Cookies.get('name') // => undefined (need to read at 'cookie.enjoytoday.cn')

关于 Internet Explorer 默认行为的注意事项:

Q3:如果我没有为 cookie 指定 DOMAIN 属性,IE 还是会将它发送到所有嵌套的子域?
答:是的,example.com 上设置的 cookie 将被发送到 sub2.sub1.example.com。
Internet Explorer 在这方面不同于其他浏览器。

Internet Explorer Cookie Internals (FAQ)

这意味着如果您省略domain属性,它将在 IE 中对子域可见。

secure


true 或 false,指示 cookie 传输是否需要安全协议 (https)。

  • 默认值:没有安全协议要求。
  • 示例
Cookies.set('name', 'value', { secure: true })
Cookies.get('name') // => 'value'
Cookies.remove('name')

sameSite

一个字符串,允许控制浏览器是否与跨站点请求一起发送 cookie。

  • 默认值:不设置
  • 示例
Cookies.set('name', 'value', { sameSite: 'strict' })
Cookies.get('name') // => 'value'
Cookies.remove('name')

设置默认值:

const api = Cookies.withAttributes({ path: '/', domain: '.example.com' })

转换

读取

创建一个覆盖默认解码实现的新 api 实例。所有依赖于正确解码才能工作的 get 方法,例如 Cookies.get() Cookies.get('name'),将为每个 cookie 运行给定的转换器。返回的值将用作 cookie 值。

读取只能使用转义函数解码的 cookie 之一的示例:

document.cookie = 'escaped=%u5317'
document.cookie = 'default=%E5%8C%97'
var cookies = Cookies.withConverter({
  read: function (value, name) {
    if (name === 'escaped') {
      return unescape(value)
    }
    // Fall back to default for all other cookies
    return Cookies.converter.read(value, name)
  }
})
cookies.get('escaped') // 北
cookies.get('default') // 北
cookies.get() // { escaped: '北', default: '北' }

写入

创建一个覆盖默认编码实现的新 api 实例:

Cookies.withConverter({
  write: function (value, name) {
    return value.toUpperCase()
  }
})

TypeScript 申明

js-cookie提供了typesrcipt的types包以供下载查阅。

$ npm i @types/js-cookie

关于

如果你还想要了解js-cookie更多信息,可参考下方信息进行获取了解:

  • github仓库地址:https://github.com/js-cookie/js-cookie
  • 百度网盘下载地址
链接:https://pan.baidu.com/s/1NLYm6mbikUF_WLpsjcQq1A
提取码:1qct

当前版本为v3.0.1, 如需更新版本请访问github自行下载。