整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          第75節 Canvas繪圖(上)-前端開發之JavaScript-王唯

          TML5新增一個元素canvas以及與這個元素伴隨而來的一套編程接口Canvas API,可以在頁面上繪制出任何想要的、動態的圖形與圖像,創造出豐富多彩的Web頁面;它可以用于動畫、游戲畫面、數據可視化、圖片編輯以及實時視頻處理等方面;

          canvas元素相當于在頁面上放置了一塊畫布,其本身并不能實現圖形繪制,也沒有任何外觀,只是一塊無色透明的區域,需要幾組API進行操作,最重要的是就是具備基本繪圖能力的2D上下文(context)及文本API,利用這些API,可以添加線條、圖片、文字、繪畫、加入高級動畫等;現在還可以使用用于繪制硬件加速的WebGL的3D上下文;

          基本用法:
          要使用canvas元素,必須先設置其width和height屬性,指定可以繪圖的區域大小;該標簽中的內容,為后備信息,當瀏覽器不支持canvas時,就會顯示這些信息,如:

          <canvas id="drawing" width="400" height="300">你的瀏覽器不支持canvas</canvas>

          canvas 的默認大小為 300px×150px,其對應的DOM元素也具有width和height屬性,并且,可以通過CSS設置其樣式;

          <style>
          #drawing{background-color: lightblue; width:400px; height:300px;}
          </style>
          <canvas id="drawing"></canvas>
          <script>
          var drawing = document.getElementById("drawing");
          console.log(drawing);
          console.log(drawing.width + ":" + drawing.height); // 還是默認的300pxX150px
          </script>

          canvas是無色透明的,如果不添加任何樣式或不繪制任何圖形,在頁面上是看不到該元素的;

          繪制圖形的步驟:
          1、取得canvas元素:

          var canvas = document.getElementsByTagName("canvas")[0];

          2、取得上下文(context):進行圖形繪制時,需要使用到圖形上下文(graphics context),圖形上下文是一個封裝了很多繪圖功能的對象;使用canvas對象的getContext()方法獲得圖形上下文,在該方法中需要傳入“2d”;

          var context = canvas.getContext("2d");

          3、設定繪圖樣式:在繪制的時候,首先要設定好繪圖的樣式,然后再調用有關方法進行圖形的繪制,如:fillStyle屬性,設定填充的樣式,在該屬性中填入顏色值;strokeStyle屬性,設置邊框的樣式,值也是顏色值;使用lineWidth屬性設置邊框的寬度;

          context.fillStyle = "yellow";
          context.strokeStyle = "green";
          context.lineWidth = 10;

          4、填充與繪制邊框:使用相應的繪制方法進行繪制,如使用fill()方法填充與stroke()方法描邊;填充是指填充圖形內部;描邊是指不填充圖形內部,只繪制圖形的邊框;對于矩形來說, fillRect()方法繪制矩形內部,strokeRect()方法繪制矩形邊框;

          context.fillRect(0,0,100,100);
          context.strokeRect(0,0,100,100);

          2D上下文(context):
          使用canvas對象的getContext()方法獲得圖形上下文,在該方法中需要傳入“2d”,就會獲取一個在2D環境中繪制圖形的CanvasRenderingContext2D類型的上下文對象,被稱為渲染上下文(The rendering context),目前只支持2D,沒有3D之類的,但不排除今后會擴展;

          // 確定瀏覽器是否支持canvas元素
          if(canvas.getContext){
          var context = canvas.getContext("2d");
          console.log(context); // CanvasRenderingContext2D
          // ...
          }

          使用2D繪圖上下文提供的方法,可以繪制簡單的2D圖形,比如矩形、弧線和路徑;如果利用它們結合不同的填充和描邊樣式,就能繪制出非常復雜的圖形;如:

          context.fillStyle = "rgba(0, 0, 200, 0.5)";
          context.fillRect (50, 50, 100, 100);

          每個canvas只能有一個上下文對象,多次調用getContext()返回的是同一個對象;

          var context = drawing.getContext("2d");
          var context1 = drawing.getContext("2d");
          console.log(context === context1); // true

          其擁有一個canvas屬性,是與當前上下文關聯的HTMLCanvasElement對象的只讀引用;

          canvas的尺寸和坐標:
          canvas默認的坐標系是以<canvas>元素的左上角(0, 0)為坐標原點;所有圖形元素的坐標都基于這個原點計算,x值越大表示越靠右,y值越大表示越靠下,反之亦然;

          畫布的尺寸和坐標系都是以CSS像素為單位的;

          默認情況下,canvas的width和height表示水平和垂直兩個方向上可用的像素數目;

          畫布的尺寸和CSS定義的尺寸是完全不同的概念:畫布的尺寸是由<canvas>元素的width和height屬性定義的,而CSS中定義的尺寸是畫布元素在頁面中顯示的尺寸;如果兩者定義的尺寸不相同,則畫布上的像素會自動縮放,以適合CSS中定義的尺寸;另外,畫布中的坐標,也是根據畫布的width和height屬性定義的;

          畫布上的點可以使用浮點數來指定坐標,但是它們不會自動轉換成整型值,畫布會采用反鋸齒的方式來模擬部分填充的像素;

          畫布的尺寸是不能隨意更改的,除非完全重置畫布;重置畫布的width和heigth屬性,會清空整個畫布,擦除當前的路徑并且會重置所有的圖形屬性(包括當前的變換和裁剪區域)為初始狀態;

          繪制基本圖形:

          填充和描邊:

          2D上下文的基本繪圖操作是填充和描邊;填充,就是用指定的樣式(顏色、漸變或圖像)填充圖形;描邊,就是只在圖形的邊緣畫線;(這個,前面講過了)

          大多數2D上下文操作都會細分為填充和描邊兩個操作,而操作的結果取決于兩個屬性:fillStyle和strokeStyle;

          這兩個屬性的值可以是字符串、漸變對象或模式對象,而且它們的默認值都是“#000000”;如果為它們指定表示顏色的字符串值,格式是與CSS一致的,如:

          // ...
          context.fillStyle = "#0000ff";
          context.strokeStyle = "red";
          context.fillRect(50,50,200,100);
          context.strokeRect(50,200,200,100);
          // ...

          繪制矩形:
          在canvas中,繪圖有兩種形式:矩形和路徑,除了矩形,所有其他類型的圖形都是通過一條或者多條路徑組合而成的;

          矩形是唯一一種可以直接在2D上下文中繪制的形狀,其分別使用fillRect(x,y,width,height)、strokeRect(x,y,width,height)方法來繪制填充矩形和繪制矩形邊框,兩者均接收4個參數:矩形的x、y坐標、矩形的寬和高,單位為像素;填充和描邊的顏色通過fillStyle和strokeStyle兩個屬性指定,如:

          function draw(id){
          var drawing = document.getElementById(id);
          if(drawing.getContext){
          var context = drawing.getContext("2d");
          // 繪制畫布背景
          context.fillStyle = "#EEEEFF";
          context.fillRect(0,0,canvas.width, canvas.height);
          // 繪制藍色矩形
          context.fillStyle = "rgba(0,0,255,0.5)";
          context.strokeStyle = "blue";
          context.fillRect(50,50,100,100);
          context.strokeRect(50,50,100,100);
          // 繪制紅色矩形
          context.strokeStyle = "#ff0000";
          context.fillStyle = "rgba(255,0,0,0.2)";
          context.strokeRect(100,100,100,100);
          context.fillRect(100,100,100,100);
          }
          }
          draw("canvas");

          示例:繪制國際象棋棋盤:

          for (i=0; i<8; i++){ // 行
          for (j=0; j<8; j++){ // 列
          if ((i+j) % 2 == 0)
          context.fillStyle = 'black';
          else
          context.fillStyle= 'white';
          context.fillRect(j*50, i*50, 50, 50);
          }
          }

          示例:繪制柱狀圖表

          var data = [100, 50, 20, 30, 100];
          var colors = ["red", "orange", "yellow", "green", "blue"];
          context.fillStyle = "white";
          context.fillRect(0, 0, canvas.width, canvas.height);
          for(var i=0; i<data.length; i++){
          var v = data[i];
          context.fillStyle = colors[i];
          context.fillRect(25+i*50, 280-v*2, 50, v*2);
          }

          關于矩形,還有一個clearRect(x, y, width, height)方法,該方法將指定的矩形區域中的圖形進行擦除,使得矩形區域中的顏色全部變為透明,其參數與前兩個方法一致;如:

          // 在兩個矩形重疊的部分清除一個小矩形
          context.clearRect(110,110,30,30);
          // 重新繪制
          context.strokeStyle = "black";
          context.fillStyle = "black";
          context.fillRect(250,25,100,100);
          context.clearRect(270,45,60,60);
          context.strokeRect(275,50,50,50);

          以上繪制的矩形,由于沒用到路徑,所以一旦繪制,會立即生效,并在canvas中渲染;

          清理畫布和恢復畫布狀態:

          <input type="button" value="清空畫布" onClick="clearMap();" />
          <script type="text/javascript">
          var c = document.getElementById("canvas");
          var context = c.getContext("2d");
          context.fillStyle = "red";
          context.strokeStyle = "yellow";
          context.beginPath();
          context.arc(200,150,100,-Math.PI*5/6,true);
          context.stroke();
          function clearMap() {
          context.clearRect(0,0,300,200);
          }
          </script>
          繪制路徑:
          圖形的基本元素是路徑,其是通過不同顏色和寬度的線段或曲線相連形成的不同形狀的點的集合;一個路徑,一般都是閉合的;
          使用路徑繪制圖形需要一些步驟:創建路徑起始點、使用相關命令畫出路徑、之后把路徑封閉;一旦路徑生成,就可以通過描邊或填充路徑區域來渲染圖形;

          2D上下文提供了多個繪制路徑的方法,通過這些方法就可以繪制出復雜的圖形和線條:
          beginPath()方法,新建一條路徑,不需要參數,表示要開始繪制新路徑,其后再調用一系列方法來實際地繪制路徑;其后繪制的路徑都是子路徑,都屬于同一條路徑;
          再使用moveTo()方法,移動當前點,它并不繪制線條,它的作用就是確定路徑起始點;
          接著使用繪圖方法,例如:lineTo()方法,可以從上一點開始繪制一條線段;或者使用rect()方法,繪制一個矩形路徑;再或者使用arc()方法,繪制一條弧線,等等;
          closePath()方法,閉合路徑;此時,就會自動繪制一條連接到路徑起點的線條,路徑的創建工作就完成了;如:

          context.beginPath();
          context.moveTo(50,50);
          context.lineTo(100,50);
          context.arc(50,50,50,0,Math.PI/2);
          context.moveTo(200,70);
          context.arc(200,70,50,0,Math.PI/2,true);
          context.rect(300,50,100,100);
          context.closePath();

          如果路徑已經完成,可以設置繪圖樣式,并進行繪圖,如用fillStyle設置填充樣式,并調用fill()方法填充,生成實心的圖形;或使用strokeStyle屬性設置邊框樣式,并調用stroke()方法對路徑描邊,即勾勒圖形輪廓;

          // ...
          context.fillStyle = "green";
          context.fill();
          context.strokeStyle = "yellow";
          context.lineWidth = 10;
          context.stroke();

          本質上,路徑是由很多子路徑構成,這些子路徑都是在一個列表中,所有的子路徑(線、弧形等)構成一個完整的路徑,當調用stroke()或fill()方法后就會繪制圖形;

          而當每次調用closePath()方法之后,列表就被清空重置,之后就可以重新繪制新的圖形,如:

          context.beginPath();
          context.moveTo(50,50);
          context.lineTo(100,50);
          context.arc(50,50,50,0,Math.PI/2);
          context.closePath();
          context.fillStyle = "red";
          context.strokeStyle = "blue";
          context.lineWidth = 12;
          context.fill();
          context.stroke();
          context.beginPath();
          context.moveTo(200,70);
          context.arc(200,70,50,0,Math.PI/2,true);
          context.closePath();
          context.fillStyle = "blue";
          context.strokeStyle = "red";
          context.lineWidth = 8;
          context.fill();
          context.stroke();
          context.beginPath();
          context.rect(300,50,100,100);
          context.closePath();
          context.fillStyle = "green";
          context.strokeStyle = "yellow";
          context.lineWidth = 4;
          context.fill();
          context.stroke();

          閉合路徑closePath()方法不是必需的,如果不使用closePath(),則路徑不閉合,如:

          context.beginPath();
          context.arc(150, 150,50, 0, Math.PI);
          // context.closePath();
          context.stroke();

          如果不使用beginPath()開始新路徑,并不會關閉之前的路徑,并且該路徑會永遠保留著;就算調用fill()方法或stroke()方法進行繪制,路徑也不會消失;因此,后續的繪制,還會重復繪制該路徑;

          移動筆觸(當前坐標點):
          moveTo(x,y)方法:將光標(筆觸)移動到指定坐標點,繪制圖形的時候就以這個坐標點為起點,也就是確定一個子路徑的起點,當個起點也稱為當前點;其并不繪制任何內容,但也屬于路徑描述的一部分;

          context.moveTo(50, 50);
          context.lineTo(150, 150);

          在畫布中第一次調用相關的繪制路徑的方法,有時候并不需要使用moveTo()指定起點位置,起點位置是由該方法的參數指定的;但后續的路徑繪制,必須要明確起點位置

          當調用beginPath()方法開始新的一段路徑時,繪制的新路徑的起點由當前的繪制方法自動確定,此時,并不需要明確調用moveTo()方法;當 canvas初始化或者beginPath()調用后,通常會使用moveTo()函數設置起點;

          示例:笑臉

          context.beginPath();
          context.arc(75, 75, 50, 0, Math.PI * 2, true);
          context.moveTo(110, 75);
          context.arc(75, 75, 35, 0, Math.PI); // 口 (順時針)
          context.moveTo(65, 65);
          context.arc(60, 65, 5, 0, Math.PI * 2, true); // 左眼
          context.moveTo(95, 65);
          context.arc(90, 65, 5, 0, Math.PI * 2, true); // 右眼
          context.closePath(); // 不是必須的
          context.stroke();

          繪制直線(線段):
          繪制線段需要lineTo()方法:lineTo(x,y)方法:指定從起點到終點(x, y)的一條直線;可重復使用lineTo方法,會以上一個lineTo()方法指定的點為起點,以當前lineTo()為終點再次創建直線;不斷重復這個過程,可以繪制復雜的圖形;

          context.moveTo(50,50);
          context.lineTo(100,100);
          context.lineTo(100,200);
          執行stroke()繪制直線;
          context.moveTo(50,50);
          context.lineTo(200,200);
          context.lineTo(50,200);
          context.stroke();

          開始點和之前的繪制路徑有關,之前路徑的結束點就是接下來的開始點,開始點也可以通過moveTo()函數改變;
          繪制直線也屬于路徑,因此在必要的情況下,也需要使用beginPath(),否則也屬于連續繪制,或調用closePath()方法閉合路徑;

          // ...
          context.beginPath();
          context.moveTo(50,50);
          context.lineTo(200,200);
          context.lineTo(50,200);
          context.lineWidth = 20;
          context.closePath();
          // ...

          示例:繪制三角形

          // 繪制三角形
          context.beginPath();
          context.moveTo(75, 50);
          context.lineTo(100, 75);
          context.lineTo(100, 25);
          context.fill();

          示例:繪制多邊形

          CanvasRenderingContext2D.prototype.polygon = function(n ,x, y, r, angle, anticlockwise){
          this.n = n || 3;
          this.x = x || 10;
          this.y = y || 10;
          this.r = r || 50;
          this.angle = angle || 0;
          this.anticlockwise = anticlockwise || false;
          
          this.moveTo(this.x + this.r * Math.sin(this.angle), this.y - this.r * Math.cos(this.angle));
          var delta = 2 * Math.PI / this.n;
          for(var i=1; i<this.n; i++){
          this.angle += this.anticlockwise ? -delta : delta;
          this.lineTo(this.x + this.r * Math.sin(this.angle), this.y - this.r * Math.cos(this.angle));
          }
          this.closePath();
          }
          var canvas = document.getElementsByTagName("canvas")[0];
          if(canvas.getContext){
          var context = canvas.getContext("2d");
          context.beginPath();
          context.polygon(3, 50, 70, 50); // 三角形
          context.polygon(4, 150, 60, 50, Math.PI / 4); // 正方形
          context.polygon(5, 255, 55, 50); // 五邊形
          context.polygon(6, 365, 53, 50, Math.PI / 6); // 六邊形
          context.polygon(4, 365, 53, 20, Math.PI / 4, true); // 六邊形中的小正方形
          context.fillStyle = "#ccc";
          context.strokeStyle = "#008";
          context.lineWidth = 5;
          context.fill();
          context.stroke();
          }
          </script>

          填充規則:
          當用到fill()、clip()和isPointinPath()時,可以選擇一個填充規則,即傳入一個fillRule參數,該填充規則根據某處在路徑的外面或者里面來決定該處是否被填充,這對于自己與自己路徑相交或者路徑被嵌套的情況是有用的;兩個可能的值:

          • "nonzero":non-zero winding rule(非零環繞規則), 默認值;
          • "evenodd":even-odd winding rule;(奇偶環繞規則)

          非零環繞原則(nonzero):
          主要解決繪圖中交叉路徑的填充問題;

          要檢測一個點P是否在路徑的內部,使用非零環繞原則:想象一條從點P出發沿著任意方向無限延伸(一直延伸到路徑所在的區域外)的射線;首先初始化一個計數器為0,然后對所有穿過這條射線的路徑進行枚舉;每當一條路徑順時針方向穿過射線的時候,計數器就加1;反之,就減1;最后,枚舉完所有路徑之后,如果計數器的值不是0,那么就認為P是在路徑內;反之,如果計數器的值是0,則認為P在路徑外;
          總之,由順、逆時針穿插次數決定是否填充某一區域;

          奇偶環繞規則(evenodd):奇數表示在路徑內,偶數表示在路徑外;
          從任意位置p作一條射線,若與該射線相交的路徑的數目為奇數,則p是路徑內部的點,否則是外部的點;

          如:

          context.beginPath();
          context.arc(50, 50, 30, 0, Math.PI*2);
          context.arc(50, 50, 15, 0, Math.PI*2, true);
          // context.fill();
          // context.fill("nonzero");
          context.fill("evenodd");

          如:繪制一個五角星

          context.arc(100, 100, 100, 0, Math.PI*2);
          context.fillStyle = '#D43D59';
          context.fill();
          
          context.beginPath();
          context.moveTo(100, 0);
          context.lineTo(100+Math.cos(Math.PI*3/10)*100, 100+Math.sin(Math.PI*3/10)*100);
          context.lineTo(100-Math.cos(Math.PI*1/10)*100, 100-Math.sin(Math.PI*1/10)*100);
          context.lineTo(100+Math.cos(Math.PI*1/10)*100, 100-Math.sin(Math.PI*1/10)*100);
          context.lineTo(100-Math.cos(Math.PI*3/10)*100, 100+Math.sin(Math.PI*3/10)*100);
          context.lineTo(100, 0);
          context.closePath();
          context.strokeStyle = "rgb(0,0,0)";
          context.fillStyle = "#246AB2"
          // context.fill('nonzero');
          context.fill('evenodd');
          context.stroke();

          如:繪制一個大箭頭圖標

          context.arc(250, 250, 150, 0, Math.PI * 2);
          context.moveTo(250, 150);
          context.lineTo(350, 250);
          context.lineTo(150, 250);
          context.closePath();
          context.moveTo(200, 250);
          context.lineTo(300, 250);
          context.lineTo(300, 350);
          context.lineTo(200, 350);
          context.closePath();
          context.fillStyle = "#0D6EB8";
          // context.fill("nonzero");
          context.fill("evenodd");

          示例:使用數學方程繪制圖案圖形

          var dx = 150, dy = 150;
          var s = 100;
          context.beginPath();
          context.fillStyle = "rgb(100,255,100)";
          context.strokeStyle = "rgb(0,0,100)";
          var dig = Math.PI / 15 * 11;
          for(var i=0; i<30; i++){
          var x = Math.sin(i * dig);
          var y = Math.cos(i * dig);
          console.log(dx + x * s, dy + y * s);
          context.lineTo(dx + x * s, dy + y * s);
          }
          context.closePath();
          // context.fill();
          context.fill("evenodd");
          context.stroke();
          // 以下包裝成一個函數
          function draw(offsetX, n){
          var dig = Math.PI / 15 * n;
          context.beginPath();
          for(var i = 0 ; i < 30 ; i++){
          var x = Math.sin(i * dig);
          var y = Math.cos(i * dig);
          context.lineTo(offsetX + x * 80, 150 + y * 80);
          }
          context.closePath();
          context.fillStyle = "green";
          // context.fill();
          context.fill("evenodd");
          context.strokeStyle = "#666";
          context.stroke();
          }
          var data = [14,13,19,7,26];
          data.forEach(function(v, i){
          draw((i + 1) * 160, v);
          });

          示例:使用鼠標實時繪制圖形

          canvas.onmousedown = function(e) {
          this.X1 = e.offsetX;
          this.Y1 = e.offsetY;
          this.isMouseDown = true;
          };
          canvas.onmousemove = function(e) {
          if (this.isMouseDown) {
          this.X2 = e.offsetX;
          this.Y2 = e.offsetY;
          this.drawing(this.X1, this.Y1, this.X2, this.Y2, e);
          }
          };
          canvas.onmouseup = function(e) {
          this.isMouseDown = false;
          };
          canvas.drawing = function (x1, y1, x2, y2, e) {
          if (!context) {
          return;
          } else {
          context.fillStyle = "red";
          context.strokeStyle = "blue";
          context.lineWidth = 5;
          context.beginPath();
          context.moveTo(x1, y1);
          context.lineTo(x2, y2);
          context.stroke();
          this.X1 = this.X2;
          this.Y1 = this.Y2;
          context.closePath();
          }
          }

          例子:迷你圖
          迷你圖(sparkline)是指用于顯示少量數據的圖形,通常會被嵌入文本流中,形如:server:小圖;迷你圖是由作者Edward Tufte杜撰的,他將該詞用于描述“內嵌在文字、數字、圖片中的小且高分辨率的圖形”;迷你圖是數據密集、設計簡單、單詞大小的圖形,如:

          <style>
          .sparkline{background-color:#ddd; color:red; display: inline-block;}
          </style>
          Load:<span class="sparkline">3 5 7 6 6 9 11 15</span>,
          source:<span class="sparkline" data-height="36" data-width="100">6 14 8 9 10 13 18</span>
          complete:<span class="sparkline" data-ymax="18" data-ymin="6">12,3,8,2,88</span>
          <script>
          window.onload = function(){
          var elts = document.getElementsByClassName("sparkline");
          main: for(var e = 0; e<elts.length; e++){
          var elt = elts[e];
          var content = elt.textContent || elt.innerText;
          var content = content.replace(/^\s+|\s+$/g, "");
          var text = content.replace(/#.*$/gm, "");
          text = text.replace(/[\n\r\t\v\f]/g, " ");
          var data = text.split(/\s+|\s*,\s*/);
          for(var i=0; i<data.length; i++){
          data[i] = Number(data[i]);
          if(isNaN(data[i]))
          continue main;
          }
          var style = getComputedStyle(elt, null);
          var color = style.color;
          var height = parseInt(elt.getAttribute("data-height")) ||
          parseInt(style.fontSize) || 20;
          var width = parseInt(elt.getAttribute("data-width")) ||
          data.length * (parseInt(elt.getAttribute("data-dx")) || 6);
          var ymin = parseInt(elt.getAttribute("data-ymin")) ||
          Math.min.apply(Math, data);
          var ymax = parseInt(elt.getAttribute("data-ymax")) ||
          Math.max.apply(Math, data);
          if(ymin >= ymax)
          ymax = ymin + 1;
          var canvas = document.createElement("canvas");
          canvas.width = width;
          canvas.height = height;
          canvas.title = content;
          elt.innerHTML = "";
          elt.appendChild(canvas);
          var context = canvas.getContext("2d");
          for(var i=0; i<data.length; i++){
          var x = width * i / data.length;
          var y = (ymax - data[i]) * height / (ymax - ymin);
          context.lineTo(x,y);
          }
          context.strokeStyle = color;
          context.stroke();
          }
          }
          </script>

          繪制曲線:
          角度和弧度:

          夾角:從一個點發射(延伸)出兩條線段,兩條線相交的部分會構成一個夾角;
          角度:兩條相交直線中的任何一條與另一條相疊合時必須轉動的量的量度,單位符號為°;
          周角:一條直線圍繞起點需要與自己相疊合時必須轉動的量的量度被稱為周角,周角等分為360度;
          弧度:角的度量單位,弧長等于半徑的弧其所對的圓心角為1弧度(弧長等于半徑時,射線夾角為1弧度);
          角度與弧度的換算公式為:弧度 = 角度 * (Math.PI / 180);

          在使用JavaScript編寫代碼進行相關計算的時候,經常需要使用Math提供的方法:

          Math.sin(弧度) 夾角對面的邊與斜邊的比值;
          Math.cos(弧度) 夾角側面的邊與斜邊的比值;

          圓形上點坐標的計算公式:
          坐標 = ( x0 + Math.cos(angle) x R,y0 + Math.sin(angle) x R )
          其中x0和y0為圓心坐標,angle為弧度,R為圓的半徑;

          例如:使用三角函數來繪制曲線:

          context.beginPath();
          for(var x = 30, y = 0; x<1000; x++){
          // 高度 * 波長 + 中心軸位置
          y = 50 * Math.sin(x / 25) + 100;
          context.lineTo(x, y);
          }
          context.stroke();

          繪制弧形和圓形:
          arc(x, y, radius, startAngle, endAngle[, anticlockwise])方法:
          此方法可以繪制一條弧,其以(x, y)為圓心,以radius為半徑繪制一條弧線(圓),從 startAngle開始到endAngle結束的弧度,按照anticlockwise給定的方向(默認為順時針)來繪制;參數anticlockwise指定是否按逆時針方向進行繪制,為true時,逆時針,反之順時針;

          // context.arc(100,100,50,0, Math.PI*3/2);
          context.arc(100,100,50,0, Math.PI*3/2,true);
          context.stroke();

          arc()方法中表示角的單位是弧度,不是角度;角度都是按順序時針計算的,無論是按時針還是按逆時針;

          由于arc()繪制的是路徑,所以在必要的情況下,需要調用beginPath();如果需要扇形,可以使用closePath()方法閉合路徑;

          當第一個調用arc()或在beginPath()方法后第一個調用arc()方法時,會在確定其圓心、半徑及開始角度的情況下,自動確定起點位置;其它情況下,需要使用moveTo()指定起點位置,例如:先將當前點和弧形的起點用一條直線連接,然后再用圓的一部分(圓弧)來連接弧形的起點和終點,并把弧形終點作為新的當前點;

          // 繪制一個圓弧
          context.beginPath();
          context.moveTo(50, 50);
          context.arc(100, 100, 50, Math.PI*3/2, 0);
          context.lineTo(150,150);
          context.stroke();
          // 繪制一個san形(楔形)
          context.beginPath();
          context.moveTo(200, 50);
          context.arc(200, 50, 50, 0, Math.PI / 2, false);
          context.closePath();
          context.stroke();
          // 同樣的san形(楔形),方向不同
          context.beginPath();
          context.moveTo(350, 100);
          context.arc(350, 100, 50, 0, Math.PI / 2, true);
          context.closePath();
          context.stroke();

          arc()不僅可以繪制圓弧,也可以用來繪制圓形,即startAngle, endAngle形成Math.PI*2(360度),如:

          context.beginPath();
          context.arc(100, 100, 80, 0, Math.PI*2);
          // context.arc(100, 100, 80, Math.PI / 4, Math.PI * 2 + Math.PI / 4);
          context.closePath(); // 沒有必要調用這一句
          context.lineWidth = 10;
          context.fillStyle = "red";
          context.fill();
          context.stroke();

          繪制有交叉路徑時弧時,要注意填充規則,如:

          context.beginPath();
          context.arc(100, 100, 50, 0, Math.PI * 2, true);
          context.arc(100, 100, 60, 0, Math.PI * 2, false);
          context.fill();
          context.beginPath();
          context.arc(300, 100, 50, 0, Math.PI, true);
          context.arc(300, 100, 60, 0, Math.PI * 2, true);
          // 使用奇偶環繞規則
          context.fill('evenodd');
          context.beginPath();
          context.arc(100, 100, 60, 0, Math.PI * 2, true);
          context.arc(140, 100, 60, 0, Math.PI * 2, false);
          context.arc(180, 100, 60, 0, Math.PI * 2, true);
          context.fill();

          循環繪制多個,如:

          for(var i=0;i<15;i++){
          context.strokeStyle = "#FF00FF";
          context.beginPath();
          context.arc(0,350,i*10,0,Math.PI*3/2,true);
          context.closePath();
          context.stroke();
          }
          for(var i=0;i<10;i++){
          context.beginPath();
          context.arc(i*25,i*25,i*10,0,Math.PI*2,true);
          context.closePath();
          context.fillStyle = 'rgba(255,0,0,0.25)';
          context.fill();
          }

          繪制不同的圓弧:

          for(var i = 0; i < 4; i++){
          for(var j = 0; j < 3; j++){
          context.beginPath();
          var x = 25 + j * 50; // x 坐標值
          var y = 25 + i * 50; // y 坐標值
          var radius = 20; // 圓弧半徑
          var startAngle = 0; // 開始點
          var endAngle = Math.PI + (Math.PI * j) / 2; // 結束點
          var anticlockwise = i % 2 == 0 ? false : true; // 順時針或逆時針
          context.arc(x, y, radius, startAngle, endAngle, anticlockwise);
          if (i>1){
          context.fill();
          } else {
          context.stroke();
          }
          }
          }

          繪制不同圓角的矩形:

          // 繪制圓角矩形
          context.beginPath();
          context.moveTo(360, 0);
          context.arc(380, 30, 30, Math.PI * 3 / 2, 0); // 上邊和右上角
          context.arc(390, 100, 20, 0, Math.PI / 2); // 右上角和右下角
          context.arc(340, 110, 10, Math.PI / 2, Math.PI); // 底邊和左下角
          context.arc(330, 0, 0, Math.PI, 0); // 左邊和左上角
          context.closePath();
          context.fill();
          context.stroke();

          示例:繪制一個WIFI圖標:

          // wifi圖標
          context.lineWidth = 3;
          for(var i=0;i<3;i++){
          context.beginPath();
          context.arc(100, 150, 15 + i*12, Math.PI, Math.PI*3/2);
          context.stroke();
          }
          context.beginPath();
          context.arc(98, 148, 3, 0, Math.PI*2);
          context.fill();

          示例:繪制一個時鐘表盤

          context.beginPath();
          // 外圓
          context.arc(100,100,99,0,Math.PI * 2, false);
          // 內圓
          context.moveTo(194, 100);
          context.arc(100,100,94,0,Math.PI * 2, false);
          // 分針
          context.moveTo(100,100);
          context.lineTo(100,15);
          // 時針
          context.moveTo(100,100);
          context.lineTo(35,100);
          context.closePath();
          context.stroke();

          示例:繪制一個手鐲

          // 使用了非零環繞規則
          function bracelet(){
          context.beginPath();
          context.arc(300,190,150,0,Math.PI*2,false); //順時針
          context.arc(300,190,100,0,Math.PI*2,true); //逆時針
          context.fillStyle="rgba(100,140,230,0.5)";
          context.strokeStyle = context.fillStyle;
          context.shadowColor="rgba(0,0,0,0.8)";
          context.shadowOffsetX = 12;
          context.shadowOffsetY = 12;
          context.shadowBlur = 15;
          context.fill();
          context.stroke();
          }
          bracelet();

          示例:繪制等分圓:

          //描邊
          drawCircle(100,100,40,2,true);
          drawCircle(200,100,40,3,true);
          drawCircle(300,100,40,4,true);
          drawCircle(400,100,40,20,true);
          drawCircle(500,100,40,100,true);
          drawCircle(600,100,40,200,true);
          //填充
          drawCircle(100,200,40,2);
          drawCircle(200,200,40,3);
          drawCircle(300,200,40,4);
          drawCircle(400,200,40,20);
          drawCircle(500,200,40,100);
          drawCircle(600,200,40,200);
          function drawCircle(x,y,r,n,isStroke){
          for(var i = 0 ; i < n ; i++){
          //計算開始和結束的角度
          var angle = 2 * Math.PI / n;
          var startAngle = angle * i;
          var endAngle = angle * (i + 1);
          context.beginPath();
          //設置繪制圓的起點
          context.moveTo(x,y);
          context.arc(x,y,r,startAngle,endAngle,false);
          if(isStroke){
          // context.strokeStyle = getRandomColor();
          context.stroke();
          }else{
          context.fillStyle = getRandomColor();
          context.fill();
          }
          }
          }
          //獲取填充的顏色/隨機
          function getRandomColor(){
          var r = getRandom();
          var g = getRandom();
          var b = getRandom();
          return "rgb("+r+","+g+","+b+")";
          }
          function getRandom(){
          return Math.floor(Math.random() * 256);
          }

          示例:繪制餅形圖

          var data = [100, 50, 20, 30, 100];
          context.fillStyle = "white";
          context.fillRect(0,0,canvas.width,canvas.height);
          var colors = [ "red","orange", "yellow","green", "blue"];
          var total = 0;
          for(var i=0; i<data.length; i++)
          total += data[i];
          var prevAngle = 0;
          for(var i=0; i<data.length; i++) {
          var fraction = data[i]/total; 
          var angle = prevAngle + fraction * Math.PI * 2;
          context.fillStyle = colors[i];
          context.beginPath();
          context.moveTo(150,150);
          context.arc(150,150, 100, prevAngle, angle, false);
          context.lineTo(150,150);
          context.fill();
          context.strokeStyle = "black";
          context.stroke();
          prevAngle = angle;
          }

          示例:餅形的類

          function PieChart(context){ 
          this.context = context || document.getElementById("canvas").getContext("2d");
          this.x = this.context.canvas.width / 2;
          this.y = this.context.canvas.height / 2;
          this.r = 120;
          this.outLine = 20;
          this.dataList = null;
          }
          PieChart.prototype = {
          constructor:PieChart,
          init:function(dataList){
          this.dataList = dataList || [{value:100}];
          
          this.transformAngle();
          this.drawPie();
          },
          drawPie:function(){
          var startAngle = 0,endAngle;
          for(var i = 0 ; i < this.dataList.length ; i++){
          var item = this.dataList[i];
          endAngle = startAngle + item.angle;
          this.context.beginPath();
          this.context.moveTo(this.x,this.y);
          this.context.arc(this.x,this.y,this.r,startAngle,endAngle,false);
          var color= this.context.strokeStyle= this.context.fillStyle= this.getRandomColor();
          this.context.stroke();
          this.context.fill();
          this.drawPieLegend(i);
          startAngle = endAngle;
          }
          },
          drawPieLegend:function(index){
          var space = 10;
          var rectW = 40;
          var rectH = 20;
          var rectX = this.x + this.r + 80;
          var rectY = this.y + (index * 30);
          this.context.fillRect(rectX,rectY,rectW,rectH);
          // this.context.beginPath();
          },
          getRandomColor:function(){
          var r = Math.floor(Math.random() * 256);
          var g = Math.floor(Math.random() * 256);
          var b = Math.floor(Math.random() * 256);
          return 'rgb('+r+','+g+','+b+')';
          },
          transformAngle:function(){
          var self = this;
          var total = 0;
          this.dataList.forEach(function(item,i){
          total += item.value;
          })
          this.dataList.forEach(function(item,i){
          self.dataList[i].angle = 2 * Math.PI * item.value/total;
          })
          },
          }
          var data = [{value:20},{value:26},
          {value:20},{value:63},{value:25}]
          var pie = new PieChart().init(data);

          arcTo(x1, y1, x2, y2, radius)方法:
          根據控制點(x1, y1)、(x2, y2)和半徑繪制圓弧路徑;其根據當前點與給定的控制點1連接的直線,和控制點1與控制點2連接的直線,作為使用指定半徑的圓的切線,畫出兩條切線之間的弧線路徑;

          說明:圖例中的x0和y0,為當前點,x1和y1代表第一個控制點的坐標,x2和y2代表第二個控制點的坐標,radius代表圓弧半徑;

          context.beginPath();
          context.moveTo(50,50);
          context.arcTo(200, 60, 250, 300, 60);
          context.stroke();
          // 繪制提示點
          context.fillRect(48,48, 4, 4); // 起點
          context.fillRect(198,58, 4, 4); // 起點
          context.fillRect(248,298, 4, 4); // 起點
          // 繪制條切線
          context.setLineDash([3]);
          context.beginPath();
          context.moveTo(50, 50);
          context.lineTo(200, 60);
          context.lineTo(250, 300);
          context.stroke();

          如果半徑radius為0,則會繪制一點直線;

          如,繪制一個圓角矩形:

          context.moveTo(400, 50);
          context.arcTo(500, 50, 500, 150, 30);
          context.arcTo(500, 150, 400, 150, 20);
          context.arcTo(400, 150, 400, 50, 10);
          context.arcTo(400, 50, 500, 50, 0);
          context.stroke();

          bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y)方法:
          貝塞爾曲線(也稱為三次貝塞爾曲線),將從當前點到指定坐標點中間的貝塞爾曲線添加到路徑中,貝塞爾曲線需要兩個控制點,分別為cp1x、cp1y和cp2x、cp2y,x和y是貝塞爾曲線的終點坐標;

          context.moveTo(100, 300);
          context.bezierCurveTo(160,200,280,300,320,250)
          context.stroke();
          context.fillRect(97, 297, 6, 6); // 起點
          context.fillRect(317, 247, 6, 6); // 終點
          context.fillRect(160-3, 200-3, 6,6); // 標記控制點
          context.fillRect(280-3, 300-3, 6,6); // 標記控制點
          // 線
          context.beginPath();
          context.strokeStyle = "red";
          context.moveTo(100, 300);
          context.lineTo(160, 200);
          context.moveTo(320, 250);
          context.lineTo(280, 300);
          context.stroke();

          結合正反弦函數繪圖:

          var dx = dy = 150;
          var s = 100;
          context.beginPath();
          context.fillStyle = "lightgreen";
          var dig = Math.PI / 15 * 11;
          context.moveTo(dx,dy);
          for(var i=0; i<30; i++){
          var x = Math.sin(i * dig);
          var y = Math.cos(i * dig);
          context.bezierCurveTo(dx+x*s, dy+y*s-100, dx+x*s+100, dy+y*s, dx+x*s, dy+y*s);
          }
          context.closePath();
          context.fill();
          context.stroke();

          示例:繪制心形

          //三次貝塞爾曲線,繪制心形
          context.beginPath();
          context.moveTo(75, 40);
          context.bezierCurveTo(75, 37, 70, 25, 50, 25);
          context.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5);
          context.bezierCurveTo(20, 80, 40, 102, 75, 120);
          context.bezierCurveTo(110, 102, 130, 80, 130, 62.5);
          context.bezierCurveTo(130, 62.5, 130, 25, 100, 25);
          context.bezierCurveTo(85, 25, 75, 37, 75, 40);
          context.fill();

          quadraticCurveTo(cx, cy, x, y)方法:
          繪制二次貝塞爾曲線,相對來說,二次貝塞爾曲線的繪制比貝塞爾曲線的繪制容易一些,因為繪制貝塞爾曲線需要兩個控制點,而繪制二次貝塞爾曲線時只需要一個控制點;因此quadraticCurveTo方法只需要四個參數就可以了,分別是控制點的坐標(cx, cy)、二次貝塞爾曲線終點的坐標(x,y);

          context.moveTo(75, 250);
          context.quadraticCurveTo(100,200,175,250);
          context.stroke();
          context.fillRect(72, 247, 6, 6); // 起點
          context.fillRect(172, 247, 6, 6); // 起點
          context.fillRect(100-3, 200-3, 6,6); // 控制點
          // 線
          context.beginPath();
          context.strokeStyle = "red";
          context.moveTo(75, 250);
          context.lineTo(100, 200);
          context.lineTo(175, 250);
          context.stroke();

          示例:對話氣泡

          context.beginPath();
          context.moveTo(75, 25);
          context.quadraticCurveTo(25, 25, 25, 62.5);
          context.quadraticCurveTo(25, 100, 50, 100);
          context.quadraticCurveTo(50, 120, 30, 125);
          context.quadraticCurveTo(60, 120, 65, 100);
          context.quadraticCurveTo(125, 100, 125, 62.5);
          context.quadraticCurveTo(125, 25, 75, 25);
          context.stroke();

          繪制圓弧或橢圓:
          ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)方法:
          繪制一條橢圓路徑;橢圓的圓心在(x,y)位置,半徑分別是radiusX 和 radiusY ,按照anticlockwise (默認順時針)指定的方向,從 startAngle 開始繪制,到 endAngle 結束;參數rotation表示橢圓的旋轉角度(單位是度,不是弧度),而startAngle和endAngle單位是弧度;

          context.lineWidth = 20;
          context.beginPath();
          context.ellipse(200,100,100,50,0,0,Math.PI*2);
          context.stroke();
          context.beginPath();
          context.ellipse(200,250,50,30,Math.PI / 4,0,Math.PI*2,true);
          context.stroke();

          如果radiusX與radiusY的值一致,繪制出來的是一個正圓;

          // 把上例代碼改成
          context.ellipse(i*25,i*25,i*10,i*20,30,0,Math.PI*2,true);

          該方法一開始不是標準方法,現在還未完全標準化,一開始只有Chrome支持,IE不支持此方法;

          參數方程法繪制橢圓:

          function ParamEllipse(context, x, y, a, b){
          var step = (a > b ) ? 1 / a : 1 / b;
          context.beginPath();
          context.moveTo(x + a, y);
          for(var i=0; i<2*Math.PI; i += step){
          context.lineTo(x + a * Math.cos(i), y + b * Math.sin(i));
          }
          context.closePath();
          context.stroke();
          }
          context.lineWidth = 10;
          ParamEllipse(context, 130, 80, 100,20);

          均勻壓縮法繪制橢圓:

          function EvenCompEllipse(context, x, y, a, b){
          context.save();
          var r = (a > b) ? a : b;
          var ratioX = a / r; // x軸縮放比
          var ratioY = b / r; // y軸縮放比
          context.scale(ratioX, ratioY); // 進行縮放(均勻壓縮)
          context.beginPath();
          // 從橢圓的左端點開始逆時針繪制
          context.moveTo((x + a) / ratioX, y / ratioY);
          context.arc(x / ratioX, y / ratioY, r, 0 , Math.PI*2);
          context.closePath();
          context.stroke();
          context.restore();
          }
          context.lineWidth = 10;
          EvenCompEllipse(context, 130, 200, 100, 20);

          使用三次貝塞爾曲線模擬橢圓:

          function BezierEllipse(context, x, y, a, b){
          // 0.5和0.6是兩個關鍵系數
          var ox = 0.5 * a, oy = 0.6 * b;
          context.save();
          context.translate(x, y);
          context.beginPath();
          // 從橢圓的下端點開始逆時針繪制
          context.moveTo(0, b);
          context.bezierCurveTo(ox, b, a, oy, a, 0);
          context.bezierCurveTo(a, -oy, ox, -b, 0, -b);
          context.bezierCurveTo(-ox, -b, -a, -oy, -a, 0);
          context.bezierCurveTo(-a, oy, -ox, b, 0, b);
          context.closePath();
          context.stroke();
          context.restore();
          }
          context.lineWidth = 10;
          BezierEllipse(context, 470, 80, 100, 20);

          二次貝塞爾曲線模擬橢圓:

          // 貝塞爾控制點x=(橢圓寬度/0.75)/2
          CanvasRenderingContext2D.prototype.oval = function (x, y, width, height) {
          var k = (width/0.75)/2,
          w = width/2,
          h = height/2;
          this.beginPath();
          this.moveTo(x, y-h);
          this.bezierCurveTo(x+k, y-h, x+k, y+h, x, y+h);
          this.bezierCurveTo(x-k, y+h, x-k, y-h, x, y-h);
          this.closePath(); return this;
          }
          context.oval(300,100,200,50);
          context.lineWidth = 10;
          context.stroke();

          rect(x, y, width, height)方法:
          繪制一個左上角坐標為(x,y),寬高為width以及height的矩形路徑,其路徑會自動閉合;
          當該方法執行的時候,moveTo()方法自動設置坐標參數(0,0),即當前筆觸自動重置回默認坐標;

          context.rect(10, 10, 100, 100);
          context.fill();

          roundrect(x, y, width, height)方法:
          這是由Chrome實現的繪制圓角矩形的方法,其它瀏覽器都不支持,如:

          context.beginPath();
          context.roundRect(50,40,100,100,50);
          context.closePath();
          context.stroke();

          兼容處理:

          if(!CanvasRenderingContext2D.prototype.roundRect){
          CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r){
          this.x = x;
          this.y = y;
          this.w = w;
          this.h = h;
          this.r = r;
          
          this.moveTo(this.x + this.r ,this.y);
          this.arcTo(this.x + this.w , this.y , this.x + this.w , this.y + this.h , this.r);
          this.arcTo(this.x + this.w , this.y + this.h , this.x , this.y + this.h , this.r);
          this.arcTo(this.x , this.y + this.h , this.x , this.y , this.r);
          this.arcTo(this.x , this.y , this.x + this.r , this.y , this.r);
          }
          }
          var canvas = document.getElementsByTagName("canvas")[0];
          if(canvas.getContext){
          var context = canvas.getContext("2d");
          context.beginPath();
          context.roundRect(50,40,100,100,50);
          context.closePath();
          context.stroke();
          context.beginPath();
          context.roundRect(200,40,100,100,50);
          context.closePath();
          context.fill();
          
          context.beginPath();
          context.roundRect(350,40,100,100,10);
          context.stroke();
          
          context.beginPath();
          context.roundRect(500,40,100,100,20);
          context.closePath();
          context.fill();
          
          context.beginPath();
          context.roundRect(650,40,120,100,30);
          context.closePath();
          context.stroke();
          }

          組合應用:

          var canvas = document.getElementsByTagName("canvas")[0];
          if(canvas.getContext){
          var context = canvas.getContext("2d");
          roundedRect(context, 12, 12, 150, 150, 15);
          roundedRect(context, 19, 19, 150, 150, 9);
          roundedRect(context, 53, 53, 49, 33, 10);
          roundedRect(context, 53, 119, 49, 16, 6);
          roundedRect(context, 135, 53, 49, 33, 10);
          roundedRect(context, 135, 119, 25, 49, 10);
          context.beginPath();
          context.arc(37, 37, 13, Math.PI / 7, -Math.PI / 7);
          context.lineTo(31, 37);
          context.fill();
          for(var i = 0; i < 8; i++){
          context.fillRect(51 + i * 16, 35, 4, 4);
          }
          for(i = 0; i < 6; i++){
          context.fillRect(115, 51 + i * 16, 4, 4);
          }
          for(i = 0; i < 8; i++){
          context.fillRect(51 + i * 16, 99, 4, 4);
          }
          context.beginPath();
          context.moveTo(83, 116);
          context.lineTo(83, 102);
          context.bezierCurveTo(83, 94, 89, 88, 97, 88);
          context.bezierCurveTo(105, 88, 111, 94, 111, 102);
          context.lineTo(111, 116);
          context.lineTo(106.333, 111.333);
          context.lineTo(101.666, 116);
          context.lineTo(97, 111.333);
          context.lineTo(92.333, 116);
          context.lineTo(87.666, 111.333);
          context.lineTo(83, 116);
          context.fill();
          context.fillStyle = "white";
          context.beginPath();
          context.moveTo(91, 96);
          // context.bezierCurveTo(88, 96, 87, 99, 87, 101);
          // context.bezierCurveTo(87, 103, 88, 106, 91, 106);
          // context.bezierCurveTo(94, 106, 95, 103, 95, 101);
          // context.bezierCurveTo(95, 99, 94, 96, 91, 96);
          // 使用這一句代替以上的4個方法
          context.ellipse(91,101,4,5,0,0,Math.PI*2);
          context.moveTo(103, 96);
          // context.bezierCurveTo(100, 96, 99, 99, 99, 101);
          // context.bezierCurveTo(99, 103, 100, 106, 103, 106);
          // context.bezierCurveTo(106, 106, 107, 103, 107, 101);
          // context.bezierCurveTo(107, 99, 106, 96, 103, 96);
          // 使用這一句代替以上的4個方法
          context.ellipse(103,101,4,5,0,0,Math.PI*2);
          context.fill();
          context.fillStyle = "black";
          context.beginPath();
          context.arc(101, 102, 2, 0, Math.PI * 2, true);
          context.fill();
          context.beginPath();
          context.arc(89, 102, 2, 0, Math.PI * 2, true);
          context.fill();
          }
          // 封裝的一個用于繪制圓角矩形的函數
          function roundedRect(context, x, y, width, height, radius){
          context.beginPath();
          context.moveTo(x, y + radius);
          context.lineTo(x, y + height - radius);
          context.quadraticCurveTo(x, y + height, x + radius, y + height);
          context.lineTo(x + width - radius, y + height);
          context.quadraticCurveTo(x + width, y + height, x + width, y + height - radius);
          context.lineTo(x + width, y + radius);
          context.quadraticCurveTo(x + width, y, x + width - radius, y);
          context.lineTo(x + radius, y);
          context.quadraticCurveTo(x, y, x, y + radius);
          context.stroke();
          }

          Path2D:
          Canvas 2D API包含一個Path2D的接口,此接口用來聲明路徑,此路徑會被2D上下文使用; Path2D擁有2D上下文的相同的路徑方法,它允許在canvas中根據需要創建可以保留并重用的路徑;

          可以使用Path2D對象的各種方法繪制直線、矩形、圓形、橢圓以及曲線;如:moveTo(x,y)、lineTo(x,y)、rect(x,y,w,h)、arc(x,y,radius,startAngle,endAngle[,anticlockwise])、arcTo(x1,1,x2,y2,radiusX[,radius,rotation])、ellipse(x,y,radius,radius,rotation,startAngle,endAngle[,anticlockwise])、bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y)、quadraticCurveTo(cpx,cpy,x,y)、closePath()

          構造函數:Path([path | svgPath]),返回一個新的 Path2D 對象;參數path為另一個Path2D對象,這將該對象所代表的路徑復制給新創建的Path2D對象;

          new Path2D();
          new Path2D(path);
          new Path2D(d);

          還可以在Path2D的構造函數中傳遞一個代表SVG路徑的字符串;

          var path = new Path2D("M10 10 h 80 v 80 h -80 Z");
          context.fill(path);

          創建完Path2D對象后,就可以利用所有繪制路徑的方法繪制路徑,并可以調用closePath()方法閉合路徑,如:

          var path1 = new Path2D();
          path1.rect(10,10,100,100);
          var path2 = new Path2D(path1);
          path2.moveTo(220,60);
          path2.arc(170,60,50,0,Math.PI*2);

          之后可以使用context對象的fill(path)和stroke(path)方法進行填充和描邊,參數path指向的Path2D對象;

          context.stroke(path2);
          // ...
          for(var i=0;i<10;i++){
          var path = new Path2D();
          path.arc(i*25,i*25,i*10,0,Math.PI*2,true);
          path.closePath();
          context.fillStyle = "rgba(255,0,0,0.25)";
          context.fill(path);
          }

          如:

          var rectangle = new Path2D();
          rectangle.rect(10, 10, 50, 50);
          var circle = new Path2D();
          circle.moveTo(125, 35);
          circle.arc(100, 35, 25, 0, 2 * Math.PI);
          context.stroke(rectangle);
          context.fill(circle);

          addPath(path [, transform])方法:
          添加一條新路徑到對當前路徑;參數path為需要添加的 Path2D 路徑,參數transform是可選的,作為新增路徑的變換矩陣;

          var p1 = new Path2D();
          p1.rect(0,0,100,100);
          var p2 = new Path2D();
          p2.rect(0,0,100,100);
          var m = document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGMatrix();
          m.a = 1; m.b = 0;
          m.c = 0; m.d = 1;
          m.e = 300; m.f = 0;
          p1.addPath(p2, m);
          context.fill(p1);

          圖形屬性:
          圖形屬性指定了畫布的通用圖形狀態,例如fillStyle、strokeStyle等屬性,繪圖方法并沒有設置這些屬性,所以這些屬性應該在調用相關繪圖方法之前進行設置;這種將從圖形狀態和繪制指令分離的思想是畫布API很重要的概念,和在HTML文檔中應用CSS樣式來實現結構和樣式分離是類似的;

          context對象上定義了15個圖形屬性:

          • fillStyle:填充顏色、漸變和圖案
          • strokeStyle:勾勒線段時的顏色、漸變或圖案
          • linewidth屬性:指定線條的寬度,值可以是任意整數;
          • lineCap屬性:如何渲染線段的末端,即為直線添加線帽,值:butt默認值,不為直線添加線帽,round圓形線帽,square正方形線帽;
          • lineJoin屬性:如何渲染頂點,即指定兩條直線交匯時的拐角形狀,值:miter默認值,尖角;round圓角;bevel斜角;
          • miterLimit:斜接頂點的最大長度;
          • font:繪制文本時的CSS字體;
          • textAlign:文本水平對齊;
          • textBaseline:垂直對齊方式;
          • shadowBlur:陰影模糊程序;
          • shadowColor:陰影顏色;
          • shadowOffsetX:陰影水平偏移量;
          • shadowOffsetY:陰影垂直偏移量;
          • globalAlpha:繪制像素時要添加的透明度;
          • globalCompositeOperation:如何合并新的像素點和下面的像素點;

          fillStyle和strokeStyle屬性:指定了區域填充和線條勾勒的樣式;即可以指定顏色,也可以把它們設置為CanvasPattern或者CanvasGradient對象,以實現背景圖案或漸變色;
          如果使用顏色,其使用標準的CSS顏色值,默認為“#000000”;具體的顏色格式有:

          var colors = [
          "#f44", // 16進制RGB,紅色
          "#44ff44", // 16進制RRGGBB,綠色
          "rgb(60,60,255)", // 0-255之間的整數表示的RGB,藍色
          "rgb(100%, 25%, 100%)", // 百分比表示的RGB,紫色
          "rgba(100%, 25%, 100%, 0.5)", // RGB加上0-1的alpha,半透明紫色
          "rgba(0,0,0,0)", // 全透明黑色
          "transparent", // 全透明
          "hsl(60,100%, 50%)", // 全飽和黃色
          "hsl(60, 75%, 50%)", // 低包包黃色
          "hsl(60, 100%, 75%)", // 全飽和暗黃色
          "hsl(60, 100%, 25%)", // 全飽和亮黃色
          "hsla(60, 100%, 50%, 0.5)" // 全飽和黃色,50%透明
          ];

          一旦設置了strokeStyle或fillStyle的值,那么這個新值就會成為新繪制的圖形的默認值,如果要給后續繪制的圖形使用不同的顏色,需要重新設置fillStyle或strokeStyle的值;

          for (var i=0;i<6;i++){
          for (var j=0;j<6;j++){ // 255/6=42.5
          context.fillStyle = 'rgb(' + Math.floor(255-42.5*i) + ',' +
          Math.floor(255-42.5*j) + ',0)';
          context.fillRect(j*25,i*25,25,25);
          }
          }

          如:

          for (var i=0;i<6;i++){
          for (var j=0;j<6;j++){
          context.strokeStyle = 'rgb(0,' + Math.floor(255-42.5*i) + ',' +
          Math.floor(255-42.5*j) + ')';
          context.beginPath();
          context.arc(12.5+j*25,12.5+i*25,10,0,Math.PI*2,true);
          context.stroke();
          }
          }

          如:

          context.fillStyle = 'rgb(255,221,0)';
          context.fillRect(0,0,150,37.5);
          context.fillStyle = 'rgb(102,204,0)';
          context.fillRect(0,37.5,150,37.5);
          context.fillStyle = 'rgb(0,153,255)';
          context.fillRect(0,75,150,37.5);
          context.fillStyle = 'rgb(255,51,0)';
          context.fillRect(0,112.5,150,37.5);
          // 畫半透明矩形
          for (var i=0; i<10; i++){
          context.fillStyle = 'rgba(255,255,255,'+(i+1)/10+')';
          for (var j=0; j<4; j++){
          context.fillRect(5+i*14, 5+j*37.5, 14, 27.5)
          }
          }

          globalAlpha屬性:全局透明度,其值是介于0到1之間的值,用于指定所有繪制的透明度,默認值為1;如:

          context.fillStyle = "#ff0000";
          context.fillRect(50,50,100,100);
          // 修改全局透明度
          context.globalAlpha= 0.5;
          context.fillStyle = "rgba(0,0,255,1)";
          context.fillRect(50,100,100,100);

          如果將其設為0,所有繪制的圖形都會變成全透明;
          繪制的每個像素都會將其alpha值乘以設置的globalAlpha值,例如:如果設置為0.5,那么所有繪制的原本不透明的像素都會變成50%的透明度,而原本是50%透明的,就變成25%的不透明度;
          globalAlpha 屬性在需要繪制大量擁有相同透明度的圖形時候相當高效;如:

          // 畫背景
          context.fillStyle = '#FD0';
          context.fillRect(0,0,75,75);
          context.fillStyle = '#6C0';
          context.fillRect(75,0,75,75);
          context.fillStyle = '#09F';
          context.fillRect(0,75,75,75);
          context.fillStyle = '#F30';
          context.fillRect(75,75,75,75);
          context.fillStyle = '#FFF';
          context.globalAlpha = 0.2;
          // 畫半透明圓
          for (var i=0;i<7;i++){
          context.beginPath();
          context.arc(75, 75, 10+10*i, 0, Math.PI*2, true);
          context.fill();
          }

          線型Line styles:
          lineWidth屬性:指定線條的寬度,值可以是任意整數;實際上可以是小于1的小數

          context.lineWidth = 0.5;
          context.strokeRect(100,50, 300, 200);

          線可以通過stroke()、strokeRect()和strokeText()方法繪制;

          lineWidth屬性沒有相像中的那么簡單:我們可以將路徑視為一個無限細的線條,當調用stroke()方法進行輪廓描邊時,它們是處于路徑的中間,兩邊都是lineWidth寬度的一半;

          for (var i = 0; i < 10; i++){
          context.lineWidth = 1+i;
          context.beginPath();
          context.moveTo(5+i*14, 5);
          context.lineTo(5+i*14, 140);
          context.stroke();
          }

          示意圖:

          如果勾勒一條閉合的路徑并只希望線段出現在路徑之外,那么首先勾勒該路徑,然后用不透明顏色填充閉合區域,將出現在路徑內的勾勒部分隱藏;又或者如果只希望出在閉合路徑內,可以調用clip()方法;

          線段寬度是受當前坐標系變換影響的,如果通過坐標系變換來對坐標軸進行縮放,例如調用scale(2,1)方法就會對X軸進行縮放,但是對Y軸不產生影響,如此,垂直的線段要比原先和它一樣寬的水平線段寬一倍;

          lineCap屬性:為直線添加線帽,值:butt默認值,不為直線添加線帽,即在線段端直接結束;round圓形線帽,即在原端點的基礎上延長一個半圓;square正方形線帽,即在原端點的基礎上,再延長線段寬度一半的長度;如圖:

          context.beginPath();
          context.lineWidth = 10;
          context.lineCap = "round";
          context.moveTo(20,20);
          context.lineTo(20,200);
          context.stroke();
          var lineCap = ['butt','round','square'];
          // 繪制上下兩條水平線條
          context.strokeStyle = '#09f';
          context.beginPath();
          context.moveTo(60,10);
          context.lineTo(200,10);
          context.moveTo(60,140);
          context.lineTo(200,140);
          context.stroke();
          // 繪制垂直三條不同端點的線條
          context.strokeStyle = 'black';
          for (var i = 0; i < lineCap.length; i++) {
          context.lineWidth = 15;
          context.lineCap = lineCap[i];
          context.beginPath();
          context.moveTo(80+i*50,10);
          context.lineTo(80+i*50,140);
          context.stroke();
          }

          lineJoin屬性:指定兩條線段交匯時的拐角形狀,即在頂點處如何連接,值:miter默認值,尖角,表示一直延伸兩條線段的外側邊緣直到在某一點匯合;round圓角,表示將匯合的頂點變成圓形;bevel斜角,表示把匯合的頂點切除;

          context.beginPath();
          context.lineWidth = 10;
          context.lineJoin = "bevel";
          context.moveTo(20,150);
          context.lineTo(80,50);
          context.lineTo(160,150);
          context.stroke();
          // 分別使用3種不同的lineJoin
          var lineJoin = ['round','bevel','miter'];
          context.lineWidth = 10;
          for (var i = 0; i < lineJoin.length; i++) {
          context.lineJoin = lineJoin[i];
          context.beginPath();
          context.moveTo(200, 50+i*40);
          context.lineTo(250, 95+i*40);
          context.lineTo(300, 50+i*40);
          context.lineTo(350, 95+i*40);
          context.lineTo(400, 50+i*40);
          context.stroke();
          }

          miterLimit屬性:當只有lineJoin屬性值是miter時才會起作用;當兩條線段相交的夾角為銳角的時候,兩條線段的斜接部分可以變得很長,該屬性可以指定斜接部分長度的上限,默認值是10.0;

          context.lineWidth = 20;
          console.log(context.miterLimit); // 10
          context.miterLimit = 7;
          context.moveTo(50,50);
          context.lineTo(150,50);
          // context.lineTo(50,150);
          // context.lineTo(50,120);
          context.lineTo(50,80); // 夾角小,斜接長度更長
          context.stroke();

          斜接最大長度是當前線寬與miterLimit屬性值的乘積的一半,即斜接限定值(miterLimit)是斜接長度與一半線寬的比值;如果指定的miterLimit值比這個比值還小的話,最終繪制出來的頂點就會是斜切的(bevel)而不是斜接的;
          當給屬性賦值時,0、負數、 Infinity 和 NaN 都會被忽略;

          setLineDash(segments)方法:自定義虛線形狀;參數segments為一個數組,描述了線段和線段間隙的交替長度;

          context.beginPath();
          context.setLineDash([5, 15]);
          context.moveTo(0, 50);
          context.lineTo(300, 50);
          context.stroke();

          如果數組只存在一個值,則表示線段與間隙長度都等于這個值;
          如果數組中的元素超過2個,則數組中的數值數量為偶數,即第奇個數代表線段長度,第偶數個數表示間隙長度;
          如果數組中的元素超過2個,且不為偶數,會自動復制一份該值,使其成為偶數量;
          如果要切換回至實線模式,將 dash list 設置為一個空數組即可;

          context.setLineDash([5]);
          context.setLineDash([5,10,15,20]);
          // context.setLineDash([5,10,15])變成context.setLineDash([5,10,15,5,10,15]);
          context.setLineDash([5,10,15]);
          context.setLineDash([]);

          常見的虛線模式,如:

          var y = 15;
          drawDashedLine([]);
          drawDashedLine([1, 1]); // 或[1]
          drawDashedLine([10, 10]);
          drawDashedLine([20, 5]);
          drawDashedLine([15, 3, 3, 3]);
          drawDashedLine([20, 3, 3, 3, 3, 3, 3, 3]);
          drawDashedLine([12, 3, 3]); // Equals [12, 3, 3, 12, 3, 3]
          function drawDashedLine(pattern) {
          context.beginPath();
          context.setLineDash(pattern);
          context.moveTo(0, y);
          context.lineTo(300, y);
          context.stroke();
          y += 20;
          }

          getLineDash方法:返回一個包含當前虛線樣式、長度為非負偶數的數組,如果數組元素的數量是奇數,數組元素會被復制并重復,如:

          console.log(context.getLineDash()); // [12, 3, 3, 12, 3, 3]

          lineDashOffset屬性:設置虛線偏移量,值是浮點數,初始值為0.0,可以為負;正值向左偏,負值向右偏,如:

          context.lineDashOffset = 5;

          示例:螞蟻線效果:

          function draw(id){
          var canvas = document.getElementById(id);
          if(canvas==null){
          return false;
          }
          var context = canvas.getContext("2d");
          context.fillStyle = "#EEEEFF";
          context.fillRect(0,0,400,300);
          
          march(context); // [mɑ?t?]
          }
          var i=0;
          function march(context){
          i++;
          if(i>16){
          i=0;
          }
          console.log(i);
          context.clearRect(5,5,110,110);
          context.setLineDash([4,2]);
          context.lineDashOffset = -i;
          context.strokeRect(10,10,100,100);
          setTimeout(function(){
          march(context);
          },50);
          }
          draw("canvas");

          繪制漸變:
          繪制線性漸變:
          fillStyle方法除了填充指定顏色外,還可以用來指定填充的對象,比如漸變對象;
          createLinearGradient(x1, y1, x2, y2)方法:
          返回一個CanvasGradient漸變對象,參數x1與y1為漸變起始點坐標,x2和y2為結束點坐標;如:

          var gradient = context.createLinearGradient(30, 30, 70, 70);
          console.log(gradient); // CanvasGradient

          使用該對象的addColorStop(pos, color)方法指定色標,參數pos指定色標位置,即一個偏移量,值為0到1之間的浮動點,如0.5 表示顏色會出現在正中間,第二個參數color指定顏色;該方法必須調用二次以上;

          gradient.addColorStop(0, "white");
          gradient.addColorStop(1, "black");

          創建了漸變對象并指定了顏色后,再把該對象賦值給fillStyle或strokeStyle屬性,從而使用漸變來進行填充或描邊;

          context.fillStyle = gradient;
          context.strokeStyle = gradient;
          context.fillRect(0, 0, 50, 50);
          context.lineWidth = 20;
          context.strokeRect(30, 30, 50, 50);

          漸變的坐標位置是相對于畫布的,所以繪制的圖形和漸變對象的坐標必須匹配;否則,有可能只會顯示部分漸變效果;因此,確保漸變與圖形對齊非常重要,如:

          function createRectLinearGradient(context, x, y, width, height){
          return context.createLinearGradient(x, y, x + width, y + height);
          }
          // ... 使用
          var gradient = createRectLinearGradient(context, 30, 30, 50, 50);
          console.log(gradient); // CanvasGradient

          如使用兩個漸變對象分別進行填充和描邊:

          // Create gradients
          var lingrad = context.createLinearGradient(0,0,0,150);
          lingrad.addColorStop(0, '#00ABEB');
          lingrad.addColorStop(0.5, '#fff');
          lingrad.addColorStop(0.5, '#26C000');
          lingrad.addColorStop(1, '#fff');
          var lingrad2 = context.createLinearGradient(0,50,0,95);
          lingrad2.addColorStop(0.5, '#000');
          lingrad2.addColorStop(1, 'rgba(0,0,0,0)');
          context.fillStyle = lingrad;
          context.strokeStyle = lingrad2;
          context.fillRect(10,10,130,130);
          context.strokeRect(50,50,50,50);

          如繪制若干個漸變圓和矩形:

          var g = context.createLinearGradient(0,0,300,0);
          g.addColorStop(0,"rgba(0,0,255,0.5)");
          g.addColorStop(1,"rgba(255,0,0,0.5)");
          context.fillStyle = g;
          for(var i=0; i<10; i++){
          // 漸變圓
          // context.beginPath();
          // context.arc(i*25,i*25,i*10,0,Math.PI*2,true);
          // context.closePath();
          // context.fill();
          // 漸變矩形
          context.fillRect(i*25, i*25, i*10, i*10);
          }

          繪制徑向漸變:
          同理,可以使用createRadialGradient(x1, y1, radius1, x2, y2, radius2)方法來繪制徑向漸變; x1和y1為開始圓的圓心坐標,radius1為開始圓的半徑,x2和y2為結果圓的圓心坐標,radius2為結束圓的半徑;其他同線性漸變相同,也是使用addColorStop()方法添加色標;

          var g = context.createRadialGradient(300,300,100,300,300,300);
          g.addColorStop(0.0, "transparent");
          g.addColorStop(0.7, "rgba(100,100,100,.9)");
          g.addColorStop(1.0, "rgba(0,0,0,0)");
          context.fillStyle = g;
          context.fillRect(0,0,drawing.width, drawing.height);

          如,繪制漸變圓:

          var g1 = context.createRadialGradient(400,0,0,400,0,400);
          g1.addColorStop(0.1,'rgb(255,255,0)');
          g1.addColorStop(0.3,'rgb(255,0,255)');
          g1.addColorStop(1,'rgb(0,255,255)');
          context.fillStyle = g1;
          context.fillRect(0,0,400,300);
          var g2 = context.createLinearGradient(250,250,0,250,250,300);
          g2.addColorStop(0.1,"rgba(255,0,0,0.5)");
          g2.addColorStop(0.7,"rgba(255,255,0,0.5)");
          g2.addColorStop(1,"rgba(0,0,255,0.5)");
          context.fillStyle = g2;
          for(var i=0;i<10;i++){
          context.beginPath();
          context.arc(i*25,i*25,i*10,0,Math.PI*2,true);
          context.closePath();
          context.fill();
          }

          如:

          // 創建漸變
          var radgrad = context.createRadialGradient(40,40,0,52,50,30);
          radgrad.addColorStop(0, '#A7D30C');
          radgrad.addColorStop(0.9, '#019F62');
          radgrad.addColorStop(1, 'rgba(1,159,98,0)');
          var radgrad2 = context.createRadialGradient(105,105,20,112,120,50);
          radgrad2.addColorStop(0, '#FF5F98');
          radgrad2.addColorStop(0.75, '#FF0188');
          radgrad2.addColorStop(1, 'rgba(255,1,136,0)');
          var radgrad3 = context.createRadialGradient(95,15,10,102,20,40);
          radgrad3.addColorStop(0, '#00C9FF');
          radgrad3.addColorStop(0.8, '#00B5E2');
          radgrad3.addColorStop(1, 'rgba(0,201,255,0)');
          var radgrad4 = context.createRadialGradient(0,150,50,0,140,90);
          radgrad4.addColorStop(0, '#F4F201');
          radgrad4.addColorStop(0.8, '#E4C700');
          radgrad4.addColorStop(1, 'rgba(228,199,0,0)');
          // 畫圖形
          context.fillStyle = radgrad4;
          context.fillRect(0,0,150,150);
          context.fillStyle = radgrad3;
          context.fillRect(0,0,150,150);
          context.fillStyle = radgrad2;
          context.fillRect(0,0,150,150);
          context.fillStyle = radgrad;
          context.fillRect(0,0,150,150);

          模式(圖案)Pattern:
          模式就是重復的圖像,也稱為填充圖案,可以用來填充或描邊圖形;使用createPattern(image,type)方法即可創建新模式,參數image為一個<img>元素或image對象,參數type指定重復的類型,其值與CSS的background-repeat值相同:no-repeat、repeat-x、repeat-y、repeat;返回的對象是CanvasPattern類型的模式對象;
          創建完模式對象后,再賦給fillStyle即可,如:

          var image = document.getElementsByTagName("img")[0];
          var pattern = context.createPattern(image, "repeat");
          console.log(pattern); // CanvasPattern
          context.fillStyle = pattern;
          context.fillRect(50, 50, 100, 100);

          模式與漸變一樣,都是從畫布的原點(0,0)開始的;將模式對象賦給fillStyle屬性,只表示在某個特定的區域內顯示重復的圖像,而不是要從某個位置開始繪制重復的圖像;

          還可以采用一個<canvas>元素作為另外一個<canvas>元素的圖案,如:

          var offscreen = document.createElement("canvas"); // 創建一個屏幕外canvas
          offscreen.width = offscreen.height = 10; // 設置大小
          offscreen.getContext("2d").strokeRect(0,0,6,6); // 繪制
          var pattern = context.createPattern(offscreen, "repeat");
          context.fillStyle = pattern;
          context.fillRect(0,0, drawing.width,drawing.height);

          CanvasPattern類型的模式對象沒有任何屬性,只有一個setTransform()方法,其用于對圖案進行變形;
          createPattern()方法的第一個參數,也可以是一個video元素,或者另一個canvas元素

          繪制陰影:
          2D上下文可以給圖形繪制陰影效果,其使用context對象的關于陰影屬性進行設置:shadowOffsetX、shadowOffsetY:橫向或縱向位移,負值表示陰影會往上或左位移,正值則表示會往下或右位移,默認值為0;
          shadowColor:陰影顏色,默認是完全透明的,如果不指定透明度或顏色,該陰影是不可見的;
          shadowBlur:可選屬性,表示圖形陰影邊緣的模糊范圍,值設定在0到10之間;

          context.shadowOffsetX = 5;
          context.shadowOffsetY = 5;
          context.shadowColor = "rgba(0, 0, 0, 0.5)";
          context.shadowBlur = 4;
          context.fillStyle = '#FF0000';
          context.fillRect(10,10, 50, 50);
          context.fillStyle = 'rgba(0, 0, 255, 0.5)';
          context.fillRect(10, 100, 50, 50);
          context.shadowOffsetX = 10;
          context.shadowOffsetY = 10;
          context.shadowColor = "rgba(100,100,100,0.5)";
          context.shadowBlur = 7.5;
          context.translate(0,50);
          for(var i=0;i<3;i++){
          context.translate(50,50);
          createStar(context);
          context.fill();
          }
          function createStar(context){
          var dx = 100;
          var dy = 0;
          var s = 50;
          context.beginPath();
          context.fillStyle = "rgba(255,0,0,0.5)";
          var dig = Math.PI / 5 * 4;
          for(var i=0;i<5;i++){
          var x = Math.sin(i*dig);
          var y = Math.cos(i*dig);
          context.lineTo(dx+x*s,dy+y*s);
          }
          context.closePath();
          }

          shadowColor不允許使用圖案和漸變色;
          shadowOffsetX和shdowOffsetY屬性總是在默認的坐標空間中度量的,它不受rotate()和scale()方法的影響;

          保存與恢復狀態:
          使用save()和restore()兩個方法,可以分別用于保存和恢復圖形上下文的當前繪制狀態;這里的繪畫狀態指的是坐標原點、變換矩陣,以及圖形上下文對象的當前屬性值等很多內容;
          canvas狀態存儲在棧中,當調用save()方法時會將當前狀態設置保存到棧中,當多次調用save()時,會在棧中保存多個狀態,之后,在做完想做的工作后,再調用restore()從棧中取出之前保存的狀態進行恢復,即在棧結構中向前返回一級,連續調用restore()則可以逐級返回;如:

          context.fillStyle = "#FF0000";
          context.save();
          context.fillStyle = "#00FF00";
          context.translate(100, 100);
          context.save();
          context.fillStyle = "#0000FF";
          // 從點(100,100)開始繪制藍色矩形
          context.fillRect(0,0, 100, 100);
          context.restore();
          // 從點(110,110)開始繪制綠色矩形
          context.fillRect(10,10,100,200);
          context.restore();
          // 從點(0,0,)開始繪制紅色矩形
          context.fillRect(0,0,100,200);
          save()保存的只是對繪圖上下文的設置和變換,不會保存繪圖的內容;具體可以應用在以下場合:圖像或圖形變形(包括即移動,旋轉和縮放)、圖像裁剪、改變圖形上下文的屬性(strokeStyle、fillStyle、globalAlpha、lineWidth、lineCap、lineJoin、miterLimit、lineDashOfffset、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、globalCompositeOperation、font、textAlign、textBaseline、direction、imageSmothingEnabled);

          示例:

          context.fillRect(0,0,150,150); // 使用默認設置繪制一個矩形
          context.save(); // 保存默認狀態
          context.fillStyle = '#09F' // 在原有配置基礎上對顏色做改變
          context.fillRect(15,15,120,120); // 使用新的設置繪制一個矩形
          context.save(); // 保存當前狀態
          context.fillStyle = '#FFF' // 再次改變顏色配置
          context.globalAlpha = 0.5;
          context.fillRect(30,30,90,90); // 使用新的配置繪制一個矩形
          context.restore(); // 重新加載之前的顏色狀態
          context.fillRect(45,45,60,60); // 使用上一次的配置繪制一個矩形
          context.restore(); // 加載默認顏色配置
          context.fillRect(60,60,30,30); // 使用加載的配置繪制一個矩形

          繪制變換圖形:
          在繪制圖形的時候,有可能需要旋轉、縮放圖形等變形處理;

          變換方法:
          translate(dx, dy):移動坐標原點到(dx, dy)處;參數dx和dy為X軸和Y軸的偏移量;

          context.translate(100,100)
          context.strokeRect(0,0,200,100);
          context.beginPath();
          context.arc(100,100,100,0,Math.PI*2);
          context.stroke();

          示例:

          for (var i = 0; i < 3; i++) {
          for (var j = 0; j < 3; j++) {
          context.save();
          context.fillStyle = 'rgb(' + (51 * i) + ', ' + (255 - 51 * i) + ', 255)';
          context.translate(10 + j * 50, 10 + i * 50);
          context.fillRect(0, 0, 25, 25); // 只關注寬和高,不再關注起始坐標
          context.restore();
          }
          }

          在一般的使用中,在使用變換時,會與狀態的保存與恢復配合使用,便于下一次的繪制;

          rotate(angle):以坐標原點為中心點旋轉,angle為弧度,為正以順時針旋轉,為負則以逆時針旋轉;

          context.rotate(Math.PI / 4);
          context.strokeRect(200,50,200,100);
          
          context.beginPath();
          context.moveTo(200, 20);
          context.lineTo(300, 20);
          context.stroke();

          在旋轉時,是以原點為中心點旋轉的,必要時,需要移動原點,否則有可能會旋轉到畫布的外側;

          context.translate(100,100);
          context.rotate(Math.PI / 4);
          context.fillRect(0,0,200,100);

          示例:繪制若干小不同顏色的小圓

          context.translate(75,75);
          for (var i=1; i<6; i++){
          context.save();
          context.fillStyle = 'rgb('+(51*i)+','+(255-51*i)+',255)';
          for (var j=0; j<i*6; j++){
          context.rotate(Math.PI*2/(i*6));
          context.beginPath();
          context.arc(0, i*12.5, 5, 0, Math.PI*2, true);
          context.fill();
          }
          context.restore();
          }

          scale(scaleX, scaleY):放大縮小,兩個參數都是實數,值為0到1,默認值為1;可以為負數,x 為水平縮放因子,y 為垂直縮放因子,如果比1小,會縮小圖形,如果比1大會放大圖形;

          context.scale(2, 1);
          context.lineWidth = 20;
          context.strokeRect(50,50,100,50);

          默認情況下,canvas的1個單位為1個像素,如果設置的縮放因子是0.5,那么1個單位就變成對應0.5個像素,這樣繪制出來的形狀就會是原先的一半;同理,設置為2.0時,1個單位就對應變成了2像素,繪制的結果就是圖形放大了2倍;
          如果線寬為1像素,且縮放因子小于1,此時,還是1像素呈現

          context.save();
          context.scale(0.5, 1); // 線寬為1的,因子小于1的還是1像素呈現
          // context.lineWidth = 2;
          context.strokeRect(50, 50, 100, 50);
          context.restore();
          context.save();
          context.scale(10, 3);
          context.fillRect(1, 100, 10, 10);
          context.restore();

          畫布初始情況下,是以左上角坐標為原點的第一象限,如果參數為負實數,相當于以 x 或 y 軸作為對稱軸鏡像反轉;例如:

          context.translate(0, canvas.height);
          context.scale(1,-1);
          context.save();
          context.lineWidth = 20;
          context.moveTo(180,10);
          context.lineTo(330,150);
          context.lineTo(30,150);
          context.closePath();
          context.stroke(); // 變成倒三角了
          context.restore();
          context.translate(canvas.width, 0);
          context.scale(-1, 1);
          context.font = '48px serif';
          context.fillText('ZERO', 10, 200);

          變換有可能很簡單,有可能很復雜,這都要視情況而定,如繪制螺旋的長方形:

          context.fillStyle = "#EEEEFF";
          context.fillRect(0,0,400,300);
          context.translate(200,50);
          // context.strokeRect(0, 0, 4, 4); // 現原點
          context.fillStyle = 'rgba(255,0,0,0.25)';
          for(var i=0; i<50; i++){ // 把50轉成3,觀察繪制過程
          context.translate(25,25);
          // context.strokeRect(0, 0, 4, 4); // 現原點
          context.scale(0.95,0.95);
          context.rotate(Math.PI / 10);
          context.fillRect(0,0,100,50);
          }

          示例:繪制表盤和表針

          // ...把前的代碼換成以下的
          context.translate(100,100);
          context.rotate(1);
          // 分針
          context.moveTo(0, 0);
          context.lineTo(0, -85);
          // 時針
          context.moveTo(0,0);
          context.lineTo(-65,0);
          // ...

          示例:科赫雪花分形:

          var deg = Math.PI / 180;
          function snowflake(c, n, x, y, len){
          c.save();
          c.translate(x, y);
          c.moveTo(0, 0);
          draw(n);
          c.rotate(-120 * deg);
          draw(n);
          c.rotate(-120 * deg);
          draw(n);
          c.closePath();
          c.restore();
          function draw(n){
          c.save();
          if(n == 0){
          c.lineTo(len, 0);
          }else{
          c.scale(1/3, 1/3);
          draw(n - 1);
          c.rotate(60 * deg);
          draw(n - 1);
          c.rotate(-120 * deg);
          draw(n - 1);
          c.rotate(60 * deg);
          draw(n - 1);
          }
          c.restore();
          c.translate(len, 0);
          }
          }
          snowflake(context, 0, 5, 115, 125);
          snowflake(context, 1, 145, 115, 125);
          snowflake(context, 2, 285, 115, 125);
          snowflake(context, 3, 425, 115, 125);
          snowflake(context, 4, 565, 115, 125);
          context.stroke();

          矩陣變換:
          當利用坐標變換不能滿足我們的需要時,可以利用矩陣變換的技術;矩陣變換是專門用來實現圖形變形的,它與坐標一起配合使用,以達到變形的目的;
          當context創建完畢后,進行一系列的屬性設置和繪圖操作,都是使用默認的坐標系;除了這個默認的坐標系,還有一個默認的“當前變換矩陣“,其作為當前圖形狀態的一部分,定義了畫布的當前坐標系;如果不對這個變換矩陣進行修改,那么接下來繪制的圖形將以畫布的最左上角為坐標原點繪制圖形,繪制出來的圖形也不經過縮放、變形處理,會被直接繪制,但是如果對這個變換矩陣進行修改,會導致使用不同的變換矩陣應用處理,從而產生不同的效果;當前變換矩陣是用來將指定的坐標轉換成為默認坐標系中的等價坐標。

          transform(a, b, c, d, e, f),變換矩陣;
          其使用一個新的變換矩陣與當前變換矩陣進行乘法運算,該變換矩陣的描述是:
          a(m11) c(m21) e(dx)
          b(m12) d(m22) f(dy)
          0 0 1
          其中,a(m11)、b(m12)、c(m21)、d(m22)這4個參數用來決定如何變形,分別表示水平縮放、垂直傾斜、水平傾斜、垂直縮放;e(dx)和f(dy)參數表示水平和垂直移動坐標原點;

          context.save();
          context.fillStyle = "red";
          context.fillRect(0,0,100,100);
          context.fillRect(100,120,100,100);
          context.restore();
          context.translate(100,120);
          context.transform(2, Math.PI / 4, -Math.PI / 4, 0.5, 50, 50);
          context.fillRect(0,0,100,100);

          示例:通過變換繪制螺旋

          context.translate(200,50);
          for(var i=0; i<50; i++){
          context.save();
          context.transform(0.95,0,0,0.95,30,30);
          context.rotate(Math.PI/12);
          context.beginPath();
          context.fillStyle = 'rgba(255,0,0,0.5)';
          context.arc(0,0,50,0,Math.PI*2,true);
          context.closePath();
          context.fill();
          }

          示例:多條彩色弧

          var colors = ["red","orange","yellow","green","blue","navy","purple"];
          context.lineWidth = 10;
          context.transform(1,0,0,1,100,0);
          for(var i=0;i<colors.length;i++){
          context.transform(1,0,0,1,0,10);
          context.strokeStyle = colors[i];
          context.beginPath();
          context.arc(50,100,100,0,Math.PI,true);
          context.stroke();
          }

          setTransform(a, b, c, d, e, f)方法:這個方法會將當前的變形矩陣重置為默認矩陣,然后用相同的參數調用transform()方法;從根本上來說,該方法是取消了當前變形,然后再設置為指定的變形,如:

          // context.transform(1,1,0,1,0,0);
          context.setTransform(1,1,0,1,0,0); // 起到相同的效果
          context.fillRect(0,0,100,100);

          它經常被用在,臨時將畫布重置為默認坐標系,如:

          context.strokeStyle = "red";
          context.strokeRect(30,10,60,20);
          // 旋轉45度
          var rad = 45 * Math.PI / 180;
          context.setTransform(Math.cos(rad),Math.sin(rad),-Math.sin(rad),Math.cos(rad),0,0);
          context.strokeStyle = "blue";
          context.strokeRect(30,10,60,20);
          // 放大2.5倍
          context.setTransform(2.5,0,0,2.5,0,0);
          context.strokeStyle = "green";
          context.strokeRect(30,10,60,20);
          // 移動坐標原點
          context.setTransform(1,0,0,1,40,80);
          context.strokeStyle = "gray";
          context.strokeRect(30,10,60,20);
          context.save(); // 保存當前默認坐標系
          context.setTransform(1,0,0,1,0,0); // 恢復到默認坐標系
          // 使用默認的坐標進行繪圖操作
          context.restore(); // 恢復保存的坐標系
          setTransform()與transform()區別:
          // context.transform(2,0,0,1,0,0);
          context.setTransform(2,0,0,1,0,0);
          context.fillRect(0,0,100,100);
          // context.transform(2,0,0,2,0,0);
          context.setTransform(2,0,0,2,0,0);
          context.fillStyle = "red";
          context.fillRect(50,50,100,100);

          再如:

          var sin = Math.sin(Math.PI/6);
          var cos = Math.cos(Math.PI/6);
          context.translate(100, 100);
          var c = 0;
          for (var i=0; i <= 12; i++) {
          c = Math.floor(255 / 12 * i);
          context.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
          context.fillRect(0, 0, 100, 10);
          context.transform(cos, sin, -sin, cos, 0, 0);
          }
          context.setTransform(-1, 0, 0, 1, 100, 100);
          context.fillStyle = "rgba(255, 128, 255, 0.5)";
          context.fillRect(0, 50, 100, 100);

          resetTransform():方法
          重置當前變形矩陣,它和調用以下語句是一樣的:context.setTransform(1, 0, 0, 1, 0, 0);

          context.rotate(45 * Math.PI / 180);
          context.fillRect(70,0,100,30);
          context.resetTransform();

          深入討論:
          translate、scale、rotate三個方法,實際上都是隱式的修改了變換矩陣;
          translate()方法只是簡單的將坐標原點進行上下左右移動;而rotate()方法會將坐標軸根據指定的角度進行順時針旋轉;scale()方法實現對X軸或Y軸上的距離進行延長和縮短;
          假定變換后坐標系中的點的坐標是(x, y),其對應的默認坐標系的坐標為(x‘, y‘);

          調用translate(dx, dy)方法等效于:

          x’ = x + dx;
          y’ = y + dy;

          調用scale(sx, sy)方法等效于:
          x’ = sx * x;
          y’ = sy * y;

          調用rotate(a)方法,可以通過三角法則計算,如:
          x’ = x * cos(a) – y * sin(a);
          y’ = y * cos(a) + x * sin(a);

          坐標系變換是與變換順序相關的;假設從畫布默認的坐標系開始,先進行位移,再進行伸縮;如此操作后,要想將現有坐標系中的點(x, y)映射成默認坐標系中的點(x’’, y’’),必須首先應用等效縮放等式把該點映射到未縮放坐標系中的一個中間點(x’, y’),然后再使用等效的位移將中間點再映射到原來坐標系中的點(x’’, y’’),結果是:
          x’’ = sx * x + dx;
          y’’ = sy * y + dy;

          如果先調用scale()再調用translate()的話,那等效的結果就不同了,結果是:
          x’’ = sx * (x + dx);
          y’’ = sy * (y + dy);

          translate、scale、rotate三個方法都可以使用transform方法進行處理:
          translate(x, y)可以使用transform(1, 0, 0, 1, x, y)代替,參數1,0,0,1表示不對圖形進行縮放傾斜操作,只是位移x和y;
          scale(x, y)可以使用transform(x, 0, 0, y, 0, 0)代替,參數x,0,0,y表示將圖形橫向擴大x倍,縱向擴大y倍;
          rotate(angle)替換方法:
          transform(Math.cos(angle * Math.PI / 180), Math.sin(angle * Math.PI / 180),-Math.sin(angle * Math.PI / 180), Math.cos(angle * Math.PI / 180),0,0)

          其中前4個參數以三角函數的形式結合起來,共同完成圖形按angle角度的順時針旋轉處理;

          者:前端藏經閣

          轉發鏈接:https://www.yuque.com/xwifrr/qbgcq0/vkczro

          目錄

          JS正則表達式完整教程(一)「值得收藏」本篇

          JS正則表達式完整教程(二)「值得收藏」

          小編建議小伙們從第一篇開始看起,更清晰明了

          引言

          親愛的讀者朋友,如果你點開了這篇文章,說明你對正則很感興趣。

          想必你也了解正則的重要性,在我看來正則表達式是衡量程序員水平的一個側面標準。

          關于正則表達式的教程,網上也有很多,相信你也看了一些。

          與之不同的是,本文的目的是希望所有認真讀完的童鞋們,都有實質性的提高。

          本文內容共有七章,用JavaScript語言完整地討論了正則表達式的方方面面。

          如果覺得文章某塊兒沒有說明白清楚,歡迎留言,能力范圍之內,老姚必做詳細解答。

          具體章節如下:

          • 引言
          • 第一章 正則表達式字符匹配攻略
          • 第二章 正則表達式位置匹配攻略
          • 第三章 正則表達式括號的作用
          • 第四章 正則表達式回溯法原理
          • 第五章 正則表達式的拆分
          • 第六章 正則表達式的構建
          • 第七章 正則表達式編程
          • 后記

          下面簡單地說說每一章都討論了什么?

          正則是匹配模式,要么匹配字符,要么匹配位置。

          第1章和第2章以這個角度去講解了正則的基礎。

          在正則中可以使用括號捕獲數據,要么在API中進行分組引用,要么在正則里進行反向引用。

          這是第3章的主題,講解了正則中括號的作用。

          學習正則表達式,是需要了解其匹配原理的。

          第4章,講解了正則了正則表達式的回溯法原理。另外在第6章里,也講解了正則的表達式的整體工作原理。

          不僅能看懂別人的正則,還要自己會寫正則。

          第5章,是從讀的角度,去拆分一個正則表達式,而第6章是從寫的角度,去構建一個正則表達式。

          學習正則,是為了在真實世界里應用的。

          第7章講解了正則的用法,和相關API需要注意的地方。

          如何閱讀本文?

          我的建議是閱讀兩遍。第一遍,不求甚解地快速閱讀一遍。閱讀過程中遇到的問題不妨記錄下來,也許閱讀完畢后就能解決很多。然后有時間的話,再帶著問題去精讀第二遍。

          深呼吸,開始我們的正則表達式旅程吧。我在終點等你。

          第一章 正則表達式字符匹配攻略

          正則表達式是匹配模式,要么匹配字符,要么匹配位置。請記住這句話。

          然而關于正則如何匹配字符的學習,大部分人都覺得這塊比較雜亂。

          畢竟元字符太多了,看起來沒有系統性,不好記。本章就解決這個問題。

          內容包括:

          1. 兩種模糊匹配
          2. 字符組
          3. 量詞
          4. 分支結構
          5. 案例分析

          1 兩種模糊匹配

          如果正則只有精確匹配是沒多大意義的,比如/hello/,也只能匹配字符串中的"hello"這個子串。

          var regex = /hello/;
          console.log( regex.test("hello") ); 
          // => true
          復制代碼

          正則表達式之所以強大,是因為其能實現模糊匹配。

          而模糊匹配,有兩個方向上的“模糊”:橫向模糊和縱向模糊。

          1.1 橫向模糊匹配

          橫向模糊指的是,一個正則可匹配的字符串的長度不是固定的,可以是多種情況的。

          其實現的方式是使用量詞。譬如{m,n},表示連續出現最少m次,最多n次。

          比如/ab{2,5}c/表示匹配這樣一個字符串:第一個字符是“a”,接下來是2到5個字符“b”,最后是字符“c”。測試如下:

          var regex = /ab{2,5}c/g;
          var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
          console.log( string.match(regex) ); 
          // => ["abbc", "abbbc", "abbbbc", "abbbbbc"]
          復制代碼

          注意:案例中用的正則是/ab{2,5}c/g,后面多了g,它是正則的一個修飾符。表示全局匹配,即在目標字符串中按順序找到滿足匹配模式的所有子串,強調的是“所有”,而不只是“第一個”。g是單詞global的首字母。

          1.2 縱向模糊匹配

          縱向模糊指的是,一個正則匹配的字符串,具體到某一位字符時,它可以不是某個確定的字符,可以有多種可能。

          其實現的方式是使用字符組。譬如[abc],表示該字符是可以字符“a”、“b”、“c”中的任何一個。

          比如/a[123]b/可以匹配如下三種字符串:"a1b"、"a2b"、"a3b"。測試如下:

          var regex = /a[123]b/g;
          var string = "a0b a1b a2b a3b a4b";
          console.log( string.match(regex) ); 
          // => ["a1b", "a2b", "a3b"]
          復制代碼

          以上就是本章講的主體內容,只要掌握橫向和縱向模糊匹配,就能解決很大部分正則匹配問題。

          接下來的內容就是展開說了,如果對此都比較熟悉的話,可以跳過,直接看本章案例那節。

          2. 字符組

          需要強調的是,雖叫字符組(字符類),但只是其中一個字符。例如[abc],表示匹配一個字符,它可以是“a”、“b”、“c”之一。

          2.1 范圍表示法

          如果字符組里的字符特別多的話,怎么辦?可以使用范圍表示法。

          比如[123456abcdefGHIJKLM],可以寫成[1-6a-fG-M]。用連字符-來省略和簡寫。

          因為連字符有特殊用途,那么要匹配“a”、“-”、“z”這三者中任意一個字符,該怎么做呢?

          不能寫成[a-z],因為其表示小寫字符中的任何一個字符。

          可以寫成如下的方式:[-az]或[az-]或[a\-z]。即要么放在開頭,要么放在結尾,要么轉義。總之不會讓引擎認為是范圍表示法就行了。

          2.2 排除字符組

          縱向模糊匹配,還有一種情形就是,某位字符可以是任何東西,但就不能是"a"、"b"、"c"。

          此時就是排除字符組(反義字符組)的概念。例如[^abc],表示是一個除"a"、"b"、"c"之外的任意一個字符。字符組的第一位放^(脫字符),表示求反的概念。

          當然,也有相應的范圍表示法。

          2.3 常見的簡寫形式

          有了字符組的概念后,一些常見的符號我們也就理解了。因為它們都是系統自帶的簡寫形式。

          \d就是[0-9]。表示是一位數字。記憶方式:其英文是digit(數字)。

          \D就是[^0-9]。表示除數字外的任意字符。

          \w就是[0-9a-zA-Z_]。表示數字、大小寫字母和下劃線。記憶方式:w是word的簡寫,也稱單詞字符。

          \W是[^0-9a-zA-Z_]。非單詞字符。

          \s是[ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、換行符、回車符、換頁符。記憶方式:s是space character的首字母。

          \S是[^ \t\v\n\r\f]。 非空白符。

          .就是[^\n\r]。通配符,表示幾乎任意字符。換行符、回車符、行分隔符和段分隔符除外。記憶方式:想想省略號...中的每個點,都可以理解成占位符,表示任何類似的東西。

          如果要匹配任意字符怎么辦?可以使用[\d\D]、[\w\W]、[\s\S]和[^]中任何的一個。

          3. 量詞

          量詞也稱重復。掌握{m,n}的準確含義后,只需要記住一些簡寫形式。

          3.1 簡寫形式

          {m,} 表示至少出現m次。

          {m} 等價于{m,m},表示出現m次。

          ? 等價于{0,1},表示出現或者不出現。記憶方式:問號的意思表示,有嗎?

          + 等價于{1,},表示出現至少一次。記憶方式:加號是追加的意思,得先有一個,然后才考慮追加。

          * 等價于{0,},表示出現任意次,有可能不出現。記憶方式:看看天上的星星,可能一顆沒有,可能零散有幾顆,可能數也數不過來。

          3.2 貪婪匹配和惰性匹配

          看如下的例子:

          var regex = /\d{2,5}/g;
          var string = "123 1234 12345 123456";
          console.log( string.match(regex) ); 
          // => ["123", "1234", "12345", "12345"]
          復制代碼

          其中正則/\d{2,5}/,表示數字連續出現2到5次。會匹配2位、3位、4位、5位連續數字。

          但是其是貪婪的,它會盡可能多的匹配。你能給我6個,我就要5個。你能給我3個,我就3萬個。反正只要在能力范圍內,越多越好。

          我們知道有時貪婪不是一件好事(請看文章最后一個例子)。而惰性匹配,就是盡可能少的匹配:

          var regex = /\d{2,5}?/g;
          var string = "123 1234 12345 123456";
          console.log( string.match(regex) ); 
          // => ["12", "12", "34", "12", "34", "12", "34", "56"]
          復制代碼

          其中/\d{2,5}?/表示,雖然2到5次都行,當2個就夠的時候,就不再往下嘗試了。

          通過在量詞后面加個問號就能實現惰性匹配,因此所有惰性匹配情形如下:

          {m,n}? {m,}???+?*?

          對惰性匹配的記憶方式是:量詞后面加個問號,問一問你知足了嗎,你很貪婪嗎?

          4. 多選分支

          一個模式可以實現橫向和縱向模糊匹配。而多選分支可以支持多個子模式任選其一。

          具體形式如下:(p1|p2|p3),其中p1、p2和p3是子模式,用|(管道符)分隔,表示其中任何之一。

          例如要匹配"good"和"nice"可以使用/good|nice/。測試如下:

          var regex = /good|nice/g;
          var string = "good idea, nice try.";
          console.log( string.match(regex) ); 
          // => ["good", "nice"]
          復制代碼

          但有個事實我們應該注意,比如我用/good|goodbye/,去匹配"goodbye"字符串時,結果是"good":

          var regex = /good|goodbye/g;
          var string = "goodbye";
          console.log( string.match(regex) ); 
          // => ["good"]
          復制代碼

          而把正則改成/goodbye|good/,結果是:

          var regex = /goodbye|good/g;
          var string = "goodbye";
          console.log( string.match(regex) ); 
          // => ["goodbye"]
          復制代碼

          也就是說,分支結構也是惰性的,即當前面的匹配上了,后面的就不再嘗試了。

          5. 案例分析

          匹配字符,無非就是字符組、量詞和分支結構的組合使用罷了。

          下面找幾個例子演練一下(其中,每個正則并不是只有唯一寫法):

          5.1 匹配16進制顏色值

          要求匹配:

          #ffbbad

          #Fc01DF

          #FFF

          #ffE

          分析:

          表示一個16進制字符,可以用字符組[0-9a-fA-F]。

          其中字符可以出現3或6次,需要使用量詞和分支結構。

          使用分支結構時,需要注意順序。

          正則如下:

          var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
          var string = "#ffbbad #Fc01DF #FFF #ffE";
          console.log( string.match(regex) ); 
          // => ["#ffbbad", "#Fc01DF", "#FFF", "#ffE"]
          復制代碼

          5.2 匹配時間

          以24小時制為例。

          要求匹配:

          23:59

          02:07

          分析:

          共4位數字,第一位數字可以為[0-2]。

          當第1位為2時,第2位可以為[0-3],其他情況時,第2位為[0-9]。

          第3位數字為[0-5],第4位為[0-9]

          正則如下:

          var regex = /^([01][0-9]|[2][0-3]):[0-5][0-9]$/;
          console.log( regex.test("23:59") ); 
          console.log( regex.test("02:07") ); 
          // => true
          // => true復制代碼

          如果也要求匹配7:9,也就是說時分前面的0可以省略。

          此時正則變成:

          var regex = /^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[1-5][0-9])$/;
          console.log( regex.test("23:59") ); 
          console.log( regex.test("02:07") ); 
          console.log( regex.test("7:9") ); 
          // => true
          // => true
          // => true復制代碼

          5.3 匹配日期

          比如yyyy-mm-dd格式為例。

          要求匹配:

          2017-06-10

          分析:

          年,四位數字即可,可用[0-9]{4}。

          月,共12個月,分兩種情況01、02、……、09和10、11、12,可用(0[1-9]|1[0-2])。

          日,最大31天,可用(0[1-9]|[12][0-9]|3[01])。

          正則如下:

          var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
          console.log( regex.test("2017-06-10") ); 
          // => true
          復制代碼

          5.4 window操作系統文件路徑

          要求匹配:

          F:\study\javascript\regex\regular expression.pdf

          F:\study\javascript\regex\

          F:\study\javascript

          F:\

          分析:

          整體模式是: 盤符:\文件夾\文件夾\文件夾\

          其中匹配F:\,需要使用[a-zA-Z]:\,其中盤符不區分大小寫,注意\字符需要轉義。

          文件名或者文件夾名,不能包含一些特殊字符,此時我們需要排除字符組[^\:*<>|"?\r\n/]來表示合法字符。另外不能為空名,至少有一個字符,也就是要使用量詞+。因此匹配“文件夾\”,可用[^\:*<>|"?\r\n/]+\。

          另外“文件夾\”,可以出現任意詞。也就是([^\:*<>|"?\r\n/]+\)*。其中括號提供的表達式。

          路徑的最后一部分可以是“文件夾”,沒有\,因此需要添加([^\:*<>|"?\r\n/]+)?。

          最后拼接成了一個看起來比較復雜的正則:

          var regex = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/;
          console.log( regex.test("F:\\study\\javascript\\regex\\regular expression.pdf") ); 
          console.log( regex.test("F:\\study\\javascript\\regex\\") ); 
          console.log( regex.test("F:\\study\\javascript") ); 
          console.log( regex.test("F:\\") ); 
          // => true
          // => true
          // => true
          // => true復制代碼

          其中,JS中字符串表示\時,也要轉義。

          5.5 匹配id

          要求從

          <div id="container" class="main"></div>

          提取出id="container"。

          可能最開始想到的正則是:

          var regex = /id=".*"/
          var string = '<div id="container" class="main"></div>';
          console.log(string.match(regex)[0]); 
          // => id="container" class="main"
          復制代碼

          因為.是通配符,本身就匹配雙引號的,而量詞*又是貪婪的,當遇到container后面雙引號時,不會停下來,會繼續匹配,直到遇到最后一個雙引號為止。

          解決之道,可以使用惰性匹配:

          var regex = /id=".*?"/
          var string = '<div id="container" class="main"></div>';
          console.log(string.match(regex)[0]); 
          // => id="container"
          復制代碼

          當然,這樣也會有個問題。效率比較低,因為其匹配原理會涉及到“回溯”這個概念(這里也只是順便提一下,第四章會詳細說明)。可以優化如下:

          var regex = /id="[^"]*"/
          var string = '<div id="container" class="main"></div>';
          console.log(string.match(regex)[0]); 
          // => id="container"復制代碼

          第1章 小結

          字符匹配相關的案例,挺多的,不一而足。

          掌握字符組和量詞就能解決大部分常見的情形,也就是說,當你會了這二者,JS正則算是入門了。


          第二章 正則表達式位置匹配攻略

          正則表達式是匹配模式,要么匹配字符,要么匹配位置。請記住這句話。

          然而大部分人學習正則時,對于匹配位置的重視程度沒有那么高。

          本章講講正則匹配位置的總總。

          內容包括:

          1. 什么是位置?
          2. 如何匹配位置?
          3. 位置的特性
          4. 幾個應用實例分析

          1. 什么是位置呢?

          位置是相鄰字符之間的位置。比如,下圖中箭頭所指的地方:

          2. 如何匹配位置呢?

          在ES5中,共有6個錨字符:

          ^ $ \b \B (?=p) (?!p)

          2.1 ^和$

          ^(脫字符)匹配開頭,在多行匹配中匹配行開頭。

          $(美元符號)匹配結尾,在多行匹配中匹配行結尾。

          比如我們把字符串的開頭和結尾用"#"替換(位置可以替換成字符的!):

          var result = "hello".replace(/^|$/g, '#');
          console.log(result); 
          // => "#hello#"
          復制代碼

          多行匹配模式時,二者是行的概念,這個需要我們的注意:

          var result = "I\nlove\njavascript".replace(/^|$/gm, '#');
          console.log(result);
          /*
          #I#
          #love#
          #javascript#
          */
          復制代碼

          2.2 \b和\B

          \b是單詞邊界,具體就是\w和\W之間的位置,也包括\w和^之間的位置,也包括\w和$之間的位置。

          比如一個文件名是"[JS] Lesson_01.mp4"中的\b,如下:

          var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
          console.log(result); 
          // => "[#JS#] #Lesson_01#.#mp4#"
          復制代碼

          為什么是這樣呢?這需要仔細看看。

          首先,我們知道,\w是字符組[0-9a-zA-Z_]的簡寫形式,即\w是字母數字或者下劃線的中任何一個字符。而\W是排除字符組[^0-9a-zA-Z_]的簡寫形式,即\W是\w以外的任何一個字符。

          此時我們可以看看"[#JS#] #Lesson_01#.#mp4#"中的每一個"#",是怎么來的。

          • 第一個"#",兩邊是"["與"J",是\W和\w之間的位置。
          • 第二個"#",兩邊是"S"與"]",也就是\w和\W之間的位置。
          • 第三個"#",兩邊是空格與"L",也就是\W和\w之間的位置。
          • 第四個"#",兩邊是"1"與".",也就是\w和\W之間的位置。
          • 第五個"#",兩邊是"."與"m",也就是\W和\w之間的位置。
          • 第六個"#",其對應的位置是結尾,但其前面的字符"4"是\w,即\w和$之間的位置。

          知道了\b的概念后,那么\B也就相對好理解了。

          \B就是\b的反面的意思,非單詞邊界。例如在字符串中所有位置中,扣掉\b,剩下的都是\B的。

          具體說來就是\w與\w、\W與\W、^與\W,\W與$之間的位置。

          比如上面的例子,把所有\B替換成"#":

          var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#');
          console.log(result); 
          // => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"
          復制代碼

          2.3 (?=p)和(?!p)

          (?=p),其中p是一個子模式,即p前面的位置。

          比如(?=l),表示'l'字符前面的位置,例如:

          var result = "hello".replace(/(?=l)/g, '#');
          console.log(result); 
          // => "he#l#lo"
          復制代碼

          而(?!p)就是(?=p)的反面意思,比如:

          var result = "hello".replace(/(?!l)/g, '#');
          console.log(result); 
          // => "#h#ell#o#"
          復制代碼

          二者的學名分別是positive lookahead和negative lookahead。

          中文翻譯分別是正向先行斷言和負向先行斷言。

          ES6中,還支持positive lookbehind和negative lookbehind。

          具體是(?<=p)和(?<!p)。

          也有書上把這四個東西,翻譯成環視,即看看右邊或看看左邊。

          但一般書上,沒有很好強調這四者是個位置。

          比如(?=p),一般都理解成:要求接下來的字符與p匹配,但不能包括p的那些字符。

          而在本人看來(?=p)就與^一樣好理解,就是p前面的那個位置。

          3. 位置的特性

          對于位置的理解,我們可以理解成空字符""。

          比如"hello"字符串等價于如下的形式:

          "hello" == "" + "h" + "" + "e" + "" + "l" + "" + "l" + "o" + "";
          復制代碼

          也等價于:

          "hello" == "" + "" + "hello"
          復制代碼

          因此,把/^hello$/寫成/^^hello$$$/,是沒有任何問題的:

          var result = /^^hello$$$/.test("hello");
          console.log(result); 
          // => true
          復制代碼

          甚至可以寫成更復雜的:

          var result = /(?=he)^^he(?=\w)llo$\b\b$/.test("hello");
          console.log(result); 
          // => true
          復制代碼

          也就是說字符之間的位置,可以寫成多個。

          把位置理解空字符,是對位置非常有效的理解方式。

          4. 相關案例

          4.1 不匹配任何東西的正則

          讓你寫個正則不匹配任何東西

          easy,/.^/

          因為此正則要求只有一個字符,但該字符后面是開頭。

          4.2 數字的千位分隔符表示法

          比如把"12345678",變成"12,345,678"。

          可見是需要把相應的位置替換成","。

          思路是什么呢?

          4.2.1 弄出最后一個逗號

          使用(?=\d{3}$)就可以做到:

          var result = "12345678".replace(/(?=\d{3}$)/g, ',')
          console.log(result); 
          // => "12345,678"
          復制代碼

          4.2.2 弄出所有的逗號

          因為逗號出現的位置,要求后面3個數字一組,也就是\d{3}至少出現一次。

          此時可以使用量詞+:

          var result = "12345678".replace(/(?=(\d{3})+$)/g, ',')
          console.log(result); 
          // => "12,345,678"
          復制代碼

          4.2.3 匹配其余案例

          寫完正則后,要多驗證幾個案例,此時我們會發現問題:

          var result = "123456789".replace(/(?=(\d{3})+$)/g, ',')
          console.log(result); 
          // => ",123,456,789"
          復制代碼

          因為上面的正則,僅僅表示把從結尾向前數,一但是3的倍數,就把其前面的位置替換成逗號。因此才會出現這個問題。

          怎么解決呢?我們要求匹配的到這個位置不能是開頭。

          我們知道匹配開頭可以使用^,但要求這個位置不是開頭怎么辦?

          easy,(?!^),你想到了嗎?測試如下:

          var string1 = "12345678",
          string2 = "123456789";
          reg = /(?!^)(?=(\d{3})+$)/g;
          var result = string1.replace(reg, ',')
          console.log(result); 
          // => "12,345,678"
          result = string2.replace(reg, ',');
          console.log(result); 
          // => "123,456,789"
          復制代碼

          4.2.4 支持其他形式

          如果要把"12345678 123456789"替換成"12,345,678 123,456,789"。

          此時我們需要修改正則,把里面的開頭^和結尾$,替換成\b:

          var string = "12345678 123456789",
          reg = /(?!\b)(?=(\d{3})+\b)/g;
          var result = string.replace(reg, ',')
          console.log(result); 
          // => "12,345,678 123,456,789"
          復制代碼

          其中(?!\b)怎么理解呢?

          要求當前是一個位置,但不是\b前面的位置,其實(?!\b)說的就是\B。

          因此最終正則變成了:/\B(?=(\d{3})+\b)/g。

          4.3 驗證密碼問題

          密碼長度6-12位,由數字、小寫字符和大寫字母組成,但必須至少包括2種字符。

          此題,如果寫成多個正則來判斷,比較容易。但要寫成一個正則就比較困難。

          那么,我們就來挑戰一下。看看我們對位置的理解是否深刻。

          4.3.1 簡化

          不考慮“但必須至少包括2種字符”這一條件。我們可以容易寫出:

          var reg = /^[0-9A-Za-z]{6,12}$/;
          復制代碼

          4.3.2 判斷是否包含有某一種字符

          假設,要求的必須包含數字,怎么辦?此時我們可以使用(?=.*[0-9])來做。

          因此正則變成:

          var reg = /(?=.*[0-9])^[0-9A-Za-z]{6,12}$/;
          復制代碼

          4.3.3 同時包含具體兩種字符

          比如同時包含數字和小寫字母,可以用(?=.*[0-9])(?=.*[a-z])來做。

          因此正則變成:

          var reg = /(?=.*[0-9])(?=.*[a-z])^[0-9A-Za-z]{6,12}$/;
          復制代碼

          4.3.4 解答

          我們可以把原題變成下列幾種情況之一:

          1. 同時包含數字和小寫字母
          2. 同時包含數字和大寫字母
          3. 同時包含小寫字母和大寫字母
          4. 同時包含數字、小寫字母和大寫字母

          以上的4種情況是或的關系(實際上,可以不用第4條)。

          最終答案是:

          var reg = /((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A-Z]))^[0-9A-Za-z]{6,12}$/;
          console.log( reg.test("1234567") ); // false 全是數字
          console.log( reg.test("abcdef") ); // false 全是小寫字母
          console.log( reg.test("ABCDEFGH") ); // false 全是大寫字母
          console.log( reg.test("ab23C") ); // false 不足6位
          console.log( reg.test("ABCDEF234") ); // true 大寫字母和數字
          console.log( reg.test("abcdEF234") ); // true 三者都有
          復制代碼

          4.3.5 解惑

          上面的正則看起來比較復雜,只要理解了第二步,其余就全部理解了。

          /(?=.*[0-9])^[0-9A-Za-z]{6,12}$/

          對于這個正則,我們只需要弄明白(?=.*[0-9])^即可。

          分開來看就是(?=.*[0-9])和^。

          表示開頭前面還有個位置(當然也是開頭,即同一個位置,想想之前的空字符類比)。

          (?=.*[0-9])表示該位置后面的字符匹配.*[0-9],即,有任何多個任意字符,后面再跟個數字。

          翻譯成大白話,就是接下來的字符,必須包含個數字。

          4.3.6 另外一種解法

          “至少包含兩種字符”的意思就是說,不能全部都是數字,也不能全部都是小寫字母,也不能全部都是大寫字母。

          那么要求“不能全部都是數字”,怎么做呢?(?!p)出馬!

          對應的正則是:

          var reg = /(?!^[0-9]{6,12}$)^[0-9A-Za-z]{6,12}$/;
          復制代碼

          三種“都不能”呢?

          最終答案是:

          var reg = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/;
          console.log( reg.test("1234567") ); // false 全是數字
          console.log( reg.test("abcdef") ); // false 全是小寫字母
          console.log( reg.test("ABCDEFGH") ); // false 全是大寫字母
          console.log( reg.test("ab23C") ); // false 不足6位
          console.log( reg.test("ABCDEF234") ); // true 大寫字母和數字
          console.log( reg.test("abcdEF234") ); // true 三者都有
          復制代碼

          第二章小結

          位置匹配相關的案例,挺多的,不一而足。

          掌握匹配位置的這6個錨字符,給我們解決正則問題一個新工具。


          第三章 正則表達式括號的作用

          不管哪門語言中都有括號。正則表達式也是一門語言,而括號的存在使這門語言更為強大。

          對括號的使用是否得心應手,是衡量對正則的掌握水平的一個側面標準。

          括號的作用,其實三言兩語就能說明白,括號提供了分組,便于我們引用它。

          引用某個分組,會有兩種情形:在JavaScript里引用它,在正則表達式里引用它。

          本章內容雖相對簡單,但我也要寫長點。

          內容包括:

          1. 分組和分支結構
          2. 捕獲分組
          3. 反向引用
          4. 非捕獲分組
          5. 相關案例

          1. 分組和分支結構

          這二者是括號最直覺的作用,也是最原始的功能。

          1.1 分組

          我們知道/a+/匹配連續出現的“a”,而要匹配連續出現的“ab”時,需要使用/(ab)+/。

          其中括號是提供分組功能,使量詞+作用于“ab”這個整體,測試如下:

          var regex = /(ab)+/g;
          var string = "ababa abbb ababab";
          console.log( string.match(regex) ); 
          // => ["abab", "ab", "ababab"]
          復制代碼

          1.2 分支結構

          而在多選分支結構(p1|p2)中,此處括號的作用也是不言而喻的,提供了子表達式的所有可能。

          比如,要匹配如下的字符串:

          I love JavaScript

          I love Regular Expression

          可以使用正則:

          var regex = /^I love (JavaScript|Regular Expression)$/;
          console.log( regex.test("I love JavaScript") );
          console.log( regex.test("I love Regular Expression") );
          // => true
          // => true復制代碼

          如果去掉正則中的括號,即/^I love JavaScript|Regular Expression$/,匹配字符串是"I love JavaScript"和"Regular Expression",當然這不是我們想要的。

          2. 引用分組

          這是括號一個重要的作用,有了它,我們就可以進行數據提取,以及更強大的替換操作。

          而要使用它帶來的好處,必須配合使用實現環境的API。

          以日期為例。假設格式是yyyy-mm-dd的,我們可以先寫一個簡單的正則:

          var regex = /\d{4}-\d{2}-\d{2}/;
          復制代碼

          然后再修改成括號版的:

          var regex = /(\d{4})-(\d{2})-(\d{2})/;
          復制代碼

          為什么要使用這個正則呢?

          2.1 提取數據

          比如提取出年、月、日,可以這么做:

          var regex = /(\d{4})-(\d{2})-(\d{2})/;
          var string = "2017-06-12";
          console.log( string.match(regex) ); 
          // => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
          復制代碼

          match返回的一個數組,第一個元素是整體匹配結果,然后是各個分組(括號里)匹配的內容,然后是匹配下標,最后是輸入的文本。(注意:如果正則是否有修飾符g,match返回的數組格式是不一樣的)。

          另外也可以使用正則對象的exec方法:

          var regex = /(\d{4})-(\d{2})-(\d{2})/;
          var string = "2017-06-12";
          console.log( regex.exec(string) ); 
          // => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
          復制代碼

          同時,也可以使用構造函數的全局屬性至來獲取:

          var regex = /(\d{4})-(\d{2})-(\d{2})/;
          var string = "2017-06-12";
          regex.test(string); // 正則操作即可,例如
          //regex.exec(string);
          //string.match(regex);
          console.log(RegExp.$1); // "2017"
          console.log(RegExp.$2); // "06"
          console.log(RegExp.$3); // "12"
          復制代碼

          2.2 替換

          比如,想把yyyy-mm-dd格式,替換成mm/dd/yyyy怎么做?

          var regex = /(\d{4})-(\d{2})-(\d{2})/;
          var string = "2017-06-12";
          var result = string.replace(regex, "$2/$3/$1");
          console.log(result); 
          // => "06/12/2017"
          復制代碼

          其中replace中的,第二個參數里用、、指代相應的分組。等價于如下的形式:

          var regex = /(\d{4})-(\d{2})-(\d{2})/;
          var string = "2017-06-12";
          var result = string.replace(regex, function() {
              return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
          });
          console.log(result); 
          // => "06/12/2017"
          復制代碼

          也等價于:

          var regex = /(\d{4})-(\d{2})-(\d{2})/;
          var string = "2017-06-12";
          var result = string.replace(regex, function(match, year, month, day) {
              return month + "/" + day + "/" + year;
          });
          console.log(result); 
          // => "06/12/2017"
          復制代碼

          3. 反向引用

          除了使用相應API來引用分組,也可以在正則本身里引用分組。但只能引用之前出現的分組,即反向引用。

          還是以日期為例。

          比如要寫一個正則支持匹配如下三種格式:

          2016-06-12

          2016/06/12

          2016.06.12

          最先可能想到的正則是:

          var regex = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;
          var string1 = "2017-06-12";
          var string2 = "2017/06/12";
          var string3 = "2017.06.12";
          var string4 = "2016-06/12";
          console.log( regex.test(string1) ); // true
          console.log( regex.test(string2) ); // true
          console.log( regex.test(string3) ); // true
          console.log( regex.test(string4) ); // true
          復制代碼

          其中/和.需要轉義。雖然匹配了要求的情況,但也匹配"2016-06/12"這樣的數據。

          假設我們想要求分割符前后一致怎么辦?此時需要使用反向引用:

          var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
          var string1 = "2017-06-12";
          var string2 = "2017/06/12";
          var string3 = "2017.06.12";
          var string4 = "2016-06/12";
          console.log( regex.test(string1) ); // true
          console.log( regex.test(string2) ); // true
          console.log( regex.test(string3) ); // true
          console.log( regex.test(string4) ); // false
          復制代碼

          注意里面的,表示的引用之前的那個分組(-|\/|\.)。不管它匹配到什么(比如-),都匹配那個同樣的具體某個字符。

          我們知道了的含義后,那么和的概念也就理解了,即分別指代第二個和第三個分組。

          看到這里,此時,恐怕你會有三個問題。

          3.1 括號嵌套怎么辦?

          以左括號(開括號)為準。比如:

          var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
          var string = "1231231233";
          console.log( regex.test(string) ); // true
          console.log( RegExp.$1 ); // 123
          console.log( RegExp.$2 ); // 1
          console.log( RegExp.$3 ); // 23
          console.log( RegExp.$4 ); // 3
          復制代碼

          我們可以看看這個正則匹配模式:

          • 第一個字符是數字,比如說1,
          • 第二個字符是數字,比如說2,
          • 第三個字符是數字,比如說3,
          • 接下來的是,是第一個分組內容,那么看第一個開括號對應的分組是什么,是123,
          • 接下來的是,找到第2個開括號,對應的分組,匹配的內容是1,
          • 接下來的是,找到第3個開括號,對應的分組,匹配的內容是23,
          • 最后的是,找到第3個開括號,對應的分組,匹配的內容是3。

          這個問題,估計仔細看一下,就該明白了。

          3.2 表示什么呢?

          另外一個疑問可能是,即是表示第10個分組,還是和0呢?

          答案是前者,雖然一個正則里出現比較罕見。測試如下:

          var regex = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/;
          var string = "123456789# ######"
          console.log( regex.test(string) );
          // => true復制代碼

          3.3 引用不存在的分組會怎樣?

          因為反向引用,是引用前面的分組,但我們在正則里引用了不存在的分組時,此時正則不會報錯,只是匹配反向引用的字符本身。例如,就匹配""。注意""表示對"2"進行了轉意。

          var regex = /\1\2\3\4\5\6\7\8\9/;
          console.log( regex.test("\1\2\3\4\5\6\7\8\9") ); 
          console.log( "\1\2\3\4\5\6\7\8\9".split("") );
          復制代碼

          chrome瀏覽器打印的結果:

          4. 非捕獲分組

          之前文中出現的分組,都會捕獲它們匹配到的數據,以便后續引用,因此也稱他們是捕獲型分組。

          如果只想要括號最原始的功能,但不會引用它,即,既不在API里引用,也不在正則里反向引用。此時可以使用非捕獲分組(?:p),例如本文第一個例子可以修改為:

          var regex = /(?:ab)+/g;
          var string = "ababa abbb ababab";
          console.log( string.match(regex) ); 
          // => ["abab", "ab", "ababab"]
          復制代碼

          5. 相關案例

          至此括號的作用已經講完了,總結一句話,就是提供了可供我們使用的分組,如何用就看我們的了。

          5.1 字符串trim方法模擬

          trim方法是去掉字符串的開頭和結尾的空白符。有兩種思路去做。

          第一種,匹配到開頭和結尾的空白符,然后替換成空字符。如:

          function trim(str) {
              return str.replace(/^\s+|\s+$/g, '');
          }
          console.log( trim("  foobar   ") ); 
          // => "foobar"
          復制代碼

          第二種,匹配整個字符串,然后用引用來提取出相應的數據:

          function trim(str) {
              return str.replace(/^\s*(.*?)\s*$/g, "$1");
          }
          console.log( trim("  foobar   ") ); 
          // => "foobar"
          復制代碼

          這里使用了惰性匹配*?,不然也會匹配最后一個空格之前的所有空格的。

          當然,前者效率高。

          5.2 將每個單詞的首字母轉換為大寫

          function titleize(str) {
              return str.toLowerCase().replace(/(?:^|\s)\w/g, function(c) {
                  return c.toUpperCase();
              });
          }
          console.log( titleize('my name is epeli') ); 
          // => "My Name Is Epeli"
          復制代碼

          思路是找到每個單詞的首字母,當然這里不使用非捕獲匹配也是可以的。

          5.3 駝峰化

          function camelize(str) {
              return str.replace(/[-_\s]+(.)?/g, function(match, c) {
                  return c ? c.toUpperCase() : '';
              });
          }
          console.log( camelize('-moz-transform') ); 
          // => "MozTransform"
          復制代碼

          其中分組(.)表示首字母。單詞的界定是,前面的字符可以是多個連字符、下劃線以及空白符。正則后面的?的目的,是為了應對str尾部的字符可能不是單詞字符,比如str是'-moz-transform '。

          5.4 中劃線化

          function dasherize(str) {
              return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
          }
          console.log( dasherize('MozTransform') ); 
          // => "-moz-transform"
          復制代碼

          駝峰化的逆過程。

          5.5 html轉義和反轉義

          // 將HTML特殊字符轉換成等值的實體
          function escapeHTML(str) {
              var escapeChars = {
                '¢' : 'cent',
                '£' : 'pound',
                '¥' : 'yen',
                '€': 'euro',
                '?' :'copy',
                '?' : 'reg',
                '<' : 'lt',
                '>' : 'gt',
                '"' : 'quot',
                '&' : 'amp',
                '\'' : '#39'
              };
              return str.replace(new RegExp('[' + Object.keys(escapeChars).join('') +']', 'g'), function(match) {
                  return '&' + escapeChars[match] + ';';
              });
          }
          console.log( escapeHTML('<div>Blah blah blah</div>') );
          // => "<div>Blah blah blah</div>";
          復制代碼

          其中使用了用構造函數生成的正則,然后替換相應的格式就行了,這個跟本章沒多大關系。

          倒是它的逆過程,使用了括號,以便提供引用,也很簡單,如下:

          // 實體字符轉換為等值的HTML。
          function unescapeHTML(str) {
              var htmlEntities = {
                nbsp: ' ',
                cent: '¢',
                pound: '£',
                yen: '¥',
                euro: '€',
                copy: '?',
                reg: '?',
                lt: '<',
                gt: '>',
                quot: '"',
                amp: '&',
                apos: '\''
              };
              return str.replace(/\&([^;]+);/g, function(match, key) {
                  if (key in htmlEntities) {
                      return htmlEntities[key];
                  }
                  return match;
              });
          }
          console.log( unescapeHTML('<div>Blah blah blah</div>') );
          // => "<div>Blah blah blah</div>"
          復制代碼

          通過key獲取相應的分組引用,然后作為對象的鍵。

          5.6 匹配成對標簽

          要求匹配:

          <title>regular expression</title>

          <p>laoyao bye bye</p>

          不匹配:

          <title>wrong!</p>

          匹配一個開標簽,可以使用正則<[^>]+>,

          匹配一個閉標簽,可以使用<\/[^>]+>,

          但是要求匹配成對標簽,那就需要使用反向引用,如:

          var regex = /<([^>]+)>[\d\D]*<\/\1>/;
          var string1 = "<title>regular expression</title>";
          var string2 = "<p>laoyao bye bye</p>";
          var string3 = "<title>wrong!</p>";
          console.log( regex.test(string1) ); // true
          console.log( regex.test(string2) ); // true
          console.log( regex.test(string3) ); // false
          復制代碼

          其中開標簽<[^>]+>改成<([^>]+)>,使用括號的目的是為了后面使用反向引用,而提供分組。閉標簽使用了反向引用,<\/>。

          另外[\d\D]的意思是,這個字符是數字或者不是數字,因此,也就是匹配任意字符的意思。

          第三章小結

          正則中使用括號的例子那可是太多了,不一而足。

          重點理解括號可以提供分組,我們可以提取數據,應該就可以了。

          例子中的代碼,基本沒做多少分析,相信你都能看懂的。

          本篇未完結,請見下一篇

          內容是《Web前端開發之Javascript視頻》的課件,請配合大師哥《Javascript》視頻課程學習。

          JavaScript也可以針對CSS進行編程,也就是所謂的腳本化CSS;通過腳本化CSS,同樣可以達到一系列的視覺效果;

          在HTML中定義樣式的方式有3種:通過<link>元素包含外部樣式表文件、使用<style>元素定義嵌入式樣式、以及使用style特性定義特定元素的內聯樣式;

          DOM2級樣式模塊圍繞這3種應用樣式的機制提供了一套API,可以檢測瀏覽器支持DOM2級定義的CSS能力;

          var supportsDOM2CSS = document.implementation.hasFeature("CSS","2.0");
          var supportsDOM2CSS2 = document.implementation.hasFeature("CSS2","2.0");

          腳本化內聯樣式:

          腳本化CSS最簡單直接的方式就是更改文檔元素的style屬性;任何支持style特性的HTML元素在Javascript中都有一個對應的style屬性,這個style屬性比較特殊,它不是字符串,而是一個CSSStyleDeclaration對象,包含著通過HTML的style特性指定的所有樣式信息,但不包含從外部樣式表或嵌入樣式表經層疊而來的樣式;

          var mydiv = document.getElementById("mydiv");
          console.log(mydiv.style);  // CSSStyleDeclaration
          mydiv.style.fontSize = "24px";
          mydiv.style.color = "purple";

          style屬性作為CSSStyleDeclaration對象,其就代表了內聯樣式,其保存了所有的樣式屬性,但如果元素沒有設置style特性或沒有使用腳本設置該特性,style中的樣式屬性值均為空字符串;

          只要取得一個有效的DOM元素的引用,就可以隨時使用Javascript為其設置樣式;

          var myDiv = document.getElementById("myDiv");
          myDiv.style.backgroundColor = "red";
          myDiv.style.width = "100px";
          myDiv.style.height = "200px";
          myDiv.style.border = "1px solid black";

          以這種方式改變樣式時,元素的外觀會自動被更新;

          通過style對象,同樣可以取得style特性中指定的樣式;

          var mydiv = document.getElementById("mydiv");
          console.log(mydiv.style.backgroundColor);
          console.log(mydiv.style.width);
          console.log(mydiv.style.height);

          腳本化的CSS屬性的名稱和值:

          在style特性中指定的任何CSS屬性都將表現為這個style對象的相應屬性;對于使用短劃線的CSS屬性名,必須將其轉換成駝峰大小寫形式,才能通過js來訪問,如:

          CSS屬性 JS屬性
          background-imagestyle.backgroundImage
          color style.color
          display style.display
          font-family style.fontFamily

          多數情況下,都可以通過簡單地轉換屬性名的格式來實現轉換,但是,當一個CSS屬性名在Javascript中如果是保留字或關鍵字時,在其前加”css”前綴,例如float屬性,由于float是JS中的保留字,因此不能被用作屬性名,其值應為cssFloat;低版本IE支持styleFloat;

          mydiv.style.backgroundColor = "purple";
          mydiv.style.borderRightStyle = "solid";
          mydiv.style.borderRightColor = "green";
          mydiv.style.cssFloat = "left";
          mydiv.style.styleFloat = "left";

          通過CSSStyleDeclaration對象設置style屬性值時,值都應該是字符串,都應該放到引號內,并且不帶分號;

          在標準模式下,所有度量值都必須指定一個度量單位;在混合模式下,可以省略單位;

          mydiv.style.width = 300;  // 錯誤,但在混雜模式下默認為300px
          mydiv.style.width = "300";  // 錯誤,但在混雜模式下默認為300px

          通過計算得來的值,也要在最后加上單位;

          var x = 10, leftMargin=20, leftBorder=10;
          mydiv.style.left = (x + leftMargin + leftBorder) + "px";

          帶單位唯一不好的地方就是,如果要獲取這個值,參與其他的數學運算,因為其是字符串,所以不能直接使用,必須轉換為數值才能參與運算,例如使用parseInt()或parseFloat()等方法進行轉換;

          var rect = mydiv.getBoundingClientRect();
          console.log(rect);
          var borderWidth = parseFloat(mydiv.style.width);
          console.log(borderWidth);

          一些CSS屬性是復合屬性,比如:margin就是margin-top、margin-right、margin-bottom、margin-left的復合屬性;CSSStyleDeclaration對象也有與之對應的復合屬性;

          mydiv.style.margin = "20px 30px 40px 50px";
          mydiv.style.marginTop = "50px";
          mydiv.style.marginLeft = "50px";

          當設置復合屬性margin時,其會自動計算其所對應的相關聯的屬性;

          即然style是元素的特性,那么也可以通過setAttribute()、getAttribute()等方法來設置style特性;

          mydiv.setAttribute("style","width: 300px; height:200px; background-color: purple;");
          console.log(mydiv.style);
          console.log(mydiv.getAttribute("style"));

          style對象的屬性和方法:

          style對象中還定義一些屬性和方法,這些屬性和方法在提供元素的style特性值的同時,也可以修改樣式;

          cssText:能夠訪問到style特性中的CSS代碼;

          length:應用給元素的CSS屬性的數量;

          parentRule:表示CSS信息的CSSRule對象;

          getPropertyCSSValue(propertyName):返回包含給定的屬性的值CSSValue對象;已廢棄;

          getPropertyPriority(propertyName);如果給定的屬性使用了!important設置,則返回important;否則返回空字符串;

          getPropertyValue(propertyName):返回給定的屬性的值;

          item(index):返回給定位置的CSS屬性的名;

          removeProperty(propertyName):從樣式中刪除給定的屬性;

          setProperty(propertyName,value,priority):為給定的屬性設置為屬性值,并加上優先權標志(”important”或一個空字符串);

          cssText屬性:返回瀏覽器對style特性中CSS代碼的內部表示,在寫入模式下,賦給cssText的值會重寫整個style特性的值;

          mydiv.style.cssText = "width: 300px; height:200px; background-color: purple;";
          console.log(mydiv.style.cssText);
          console.log(mydiv.getAttribute("style"));

          設置cssText屬性的目的就是為元素快速應用多個CSS特性,可以一次性應用所有的變化;

          item()方法和length屬性:設計length屬性的目的,就是將其與item()方法配套使用,以便迭代元素中定義的CSS屬性名;在使用length和item()時,style對象實際上是一個集合,可以使用方括號來代替item()來取得給定位置的css屬性名;

          console.log(mydiv.style.length);
          for(var i=0; i<mydiv.style.length; i++){
              // console.log(mydiv.style.item(i));
              console.log(mydiv.style[i]);
          }

          getPropertyValue(propertyName)方法:取得給定的屬性的值;

          console.log(mydiv.style.getPropertyValue("background-color"));
          for(var i=0; i<mydiv.style.length; i++){
              var prop = mydiv.style[i];  // 取得屬性名
              var value = mydiv.style.getPropertyValue(prop);  // 取得屬性值
              console.log(prop + ":" + value);
          }

          getPropertyCSSValue(propertyName)方法:該方法會返回一個包含兩個屬性的CSSValue對象,這兩個屬性分別是:cssText和cssValueType,其中cssText屬性的值與getPropertyValue()方法返回的值相同,而cssValueType屬性則是一個數值常量,表示值的類型:0表示繼承的值,1表示基本的值,2表示值列表,3表示自定義的值;

          var value = mydiv.style.getPropertyCSSValue("background-color");
          console.log(value);
          console.log(value.cssText);
          console.log(value.cssValueType);

          這個方法已經被廢棄了,幾乎所有的瀏覽器都不支持;

          getPropertyPriority(propertyName);如果給定的屬性使用了!important設置,則返回important;否則返回空字符串;

          console.log(mydiv.style.getPropertyPriority("background-color")); // important

          setProperty(propertyName,value,priority):為給定的屬性設置為屬性值,并加上優先權標志,priority 為可選(”important”或一個空字符串);

          mydiv.style.setProperty("background-color","purple","important");

          removeProperty(propertyName)方法:可以從元素的樣式中移除某個CSS屬性,移除一個屬性,意味著該屬性將應用默認的樣式(從其他樣式表經層疊而來);

          mydiv.style.removeProperty("background-color");

          parentRule:只讀,表示CSS信息的CSSRule對象;

          var rule = mydiv.style.parentRule;
          console.log(rule);  // null

          計算的樣式:

          雖然style對象能夠提供style特性所有樣式信息,但它不包含那些從其他樣式表層疊而來并影響到當前元素的實際樣式信息;所以,需要使用計算樣式;

          計算樣式是一組屬性值,它由瀏覽器通過把內聯樣式結合所有鏈接樣式表中所有可應用的樣式規則后導出(計算)得到的,也就是一組在呈現元素時實際使用的屬性;

          getComputedStyle()方法:

          該方法是window對象的方法,可以通過window.getComputedStyle(),或者document.defaultView.getComputedStyle()方法調用,而document.defaultView的返回值就是window;

          該方法接受兩個參數:要取得計算樣式的元素和一個偽元素字符串(如::after);如果不需要偽元素,第二個參數可以為null或一個空字符串;該方法返回一個CSSStyleDeclaration對象,與style屬性的類型相同,區別在于它是只讀的,其中包含當前元素的所有計算的樣式;

          <style>
              #mydiv{background-color: purple; width: 100px; height: 100px;}
          </style>
          <div id="mydiv" style="background: pink; border: 1px solid black;">mydiv</div>
          <script>
          var mydiv = document.getElementById("mydiv");
          var computedStyle = document.defaultView.getComputedStyle(mydiv,null);
          console.log(computedStyle);
          console.log(computedStyle.backgroundColor);
          console.log(computedStyle.width);
          console.log(computedStyle.height);
          console.log(computedStyle.border);
          </script>

          注:以上border屬性可能不會返回實際的border規則(如IE和Firefox返回空字符串),原因是不同瀏覽中解釋復合屬性的方式不同,因為設置這種屬性實際上會涉及很多其他屬性,例如:border,實際上調協了四個邊的邊框寬度、顏色等,因此border不會返回,但computedStyle.borderleftWidth會返回值;

          console.log(computedStyle.borderLeftWidth);
          console.log(computedStyle.borderLeftColor);

          另外,不同瀏覽器表示值的方式可能會有所區別;

          計算后的樣式也包含屬于瀏覽器內部樣式表的樣式信息,因此任何具有默認值的CSS屬性都會表現在計算后的樣式中;如visibility屬性都有一個默認值,有些瀏覽器設置為visible,而有些設置為inherit;

          計算樣式的CSSStyleDeclaration對象和表示內聯樣式的對象之間有一些重要的區別:

          計算樣式的屬性是只讀的;

          計算樣式的值是絕對值,類似百分比和點之類的相對的單位將全部轉換為絕對值;所有指定尺寸,例如外邊距大小和字體大小等屬性都有一個以像素為度量單位的值;相關顏色的屬性將以”rgb(#,#,#)”或”rgba(#,#,#,#)”的格式返回;

          不計算復合屬性,它們只基于最基礎的屬性,例如,不要查詢margin屬性,應該使用marginLeft或marginTop等;

          計算樣式的cssText屬性未定義(也就是該屬性返回空字符串);

          計算樣式和內聯樣式可以同時使用;

          // 用指定的因子縮放元素e的文本尺寸
          function scale(e, factor){
              // 用計算樣式查詢當前文本的尺寸
              var size = parseInt(window.getComputedStyle(e,"").fontSize);
              // 用內聯樣式來放大尺寸
              e.style.fontSize = factor * size + "px";
          }
          // 用指定的因子修改元素的背景顏色
          // factors > 1 顏色變淺,factors < 1顏色變暗
          function scaleColor(e, factor){
              var color = window.getComputedStyle(e,"").backgroundColor;
              var components = color.match(/[\d\.]+/g); // 解析r、g、b分量
              for(var i=0; i<3; i++){  // 循環r,g,b
                  var x = Number(components[i]) * factor;  // 縮放每個值
                  x = Math.round(Math.min(Math.max(x, 0), 255)); // 設置邊界并取整
                  components[i] = String(x);
              }
              if(components.length == 3)  // rgb()顏色
                  e.style.backgroundColor = "rgb(" + components.join() + ")";
              else  // rgba()顏色
                  e.style.backgroundColor = "rgba(" + components.join() + ")";
          }
          var mydiv = document.getElementById("mydiv");
          scale(mydiv, 1.5);
          scaleColor(mydiv, .5);

          低版本的IE不支持getComputedStyle()方法,但它有一種類似的概念;在IE中,具有style屬性的元素還有一個currentStyle屬性,該屬性是CSSStyleDeclaration的實例,包含當前元素全部計算后的樣式,但只有IE支持;

          var computedStyle = mydiv.currentStyle;
          console.log(computedStyle.backgroundColor);
          console.log(computedStyle.width);
          console.log(computedStyle.height);
          console.log(computedStyle.borderLeftWidth);

          兼容函數:

          function getStyle(obj, attr){
              if(window.getComputedStyle)
                  return getComputedStyle(obj, null)[attr];
              else
                  return obj.currentStyle[attr];
          }
          var mydiv = document.getElementById("mydiv");
          var backgroundColor = getStyle(mydiv, "background-color");
          console.log(backgroundColor);  // rgb(245, 222, 179)
          // 或者
          function getCss(obj, css){
              return (document.defaultView.getComputedStyle ? 
                  document.defaultView.getComputedStyle(obj,null) : 
                  obj.currentStyle)[css];
          }
          var borderTopWidth = getCss(mydiv, "border-top-width");
          console.log(borderTopWidth); // 1px

          封裝一下函數,用來獲取CSS屬性值,如:

          function getComputedStyles(elem,prop) {
            var cs = window.getComputedStyle(elem,null);
            if (prop) {
                  console.log(prop+" : "+cs.getPropertyValue(prop));
                  return;
            }
            var len = cs.length;
            for (var i=0;i<len;i++) {
              var style = cs[i];
              console.log(style+" : "+cs.getPropertyValue(style));
            }
          }
          getComputedStyles(mydiv);  // 查詢所有
          getComputedStyles(mydiv,"background-color");  // 只查詢一個屬性

          與偽元素一起使用:getComputedStyle可以從偽元素中提取樣式信息(例如:::after, ::before, ::marker, ::line-marker);

          <style>
              #mydiv::after{content: "大師哥王唯";}
          </style>
          <div id="mydiv"></div>
          <script>
          var mydiv = document.getElementById("mydiv");
          var computedStyle = document.defaultView.getComputedStyle(mydiv,":after");
          console.log(computedStyle.content);
          <script>

          使用計算樣式是可以獲取元素的幾何尺寸和位置的,但是其獲得的結果并不一定是我們所期望的,此時可以使用getBoundingClientRect(),其返回的值是與呈現的效果是一致的;

          console.log(computedStyle.left);  // auto
          console.log(computedStyle.top);  // auto
          console.log(computedStyle.width);  // 300px
          // left:8 top:8 width:302,包括了border
          var rect = mydiv.getBoundingClientRect();
          console.log(rect);

          腳本化CSS類:

          也可以腳本化元素的class屬性,改變元素的class就改變了應用于元素的一組樣式表選擇器,它能在同一時刻改變多個CSS屬性;

          className屬性:

          與元素的class特性對應,即為元素指定的CSS類;由于class是ECMAScript保留字,所以在Javascript中使用className;

          在操作類名時,需要通過className屬性添加、刪除和替換類名;

          var mydiv = document.getElementById("mydiv");
          mydiv.className = "container"; // 設置
          mydiv.className = ""; // 刪除
          mydiv.className = "other"; // 替換
          // 或
          if(mydiv.className == ""){
            mydiv.className = "container";
          }

          元素可以設置多個CSS類,其className中保存的是擁有多個類名的字符串,因此即使只修改字符串一部分,都會覆蓋之前的值,所以每次都必須設置整個字符串的值;

          var mydiv = document.getElementById("mydiv");
          console.log(mydiv.className);  // db user disable
          mydiv.className = "container";
          console.log(mydiv.className); // container

          如果要從class中只刪除一個類名,需要把多個類名拆開,刪除不想要的那個,然后再把其他類名拼成一個新字符串,如:

          // 如,刪除user類
          // 首先,取得類名字符串并拆分成數組
          var mydiv = document.getElementById("mydiv");
          var classNames = mydiv.className.split(/\s+/);
          // 找到要刪的類名
          var pos = -1, i, len;
          for(i=0, len = classNames.length; i<len; i++){
              if(classNames[i] == "user"){
                  pos = i;
                  break;
              }
          }
          // 刪除類名
          classNames.splice(i,1);
          // 把剩下的類名拼成字符串并重新設置
          mydiv.className = classNames.join(" ");

          如果要添加類名,是可以直接通過拼接字符串完成,但在拼接之前,必須要通過檢測確定不會多次添加相同的類名;

          Element.classList屬性:

          HTML5新增了一種操作類名的方式,可以讓操作更簡單也更安全,即classList屬性,其是一個DOMTokenList對象,其是只讀的類數組對象,與其他DOM集合類似,它也有一個表示自己包含多少個元素的length屬性,而要取得每個元素可以使用item()方法,也可以使用方括號語法,此外,這個新類型還定義如下的方法:

          • add(value):將給定的字符串值添加到列表中,如果值存在,就不添加;
          • contains(value):表示列表中是否存在給定的值,如果存在返回true,否則,返回false;
          • remove(value):從列表中刪除給定的字符串;
          • toggle(value):如果列表中已經存在給定的值,刪除它,否則,添加它;
          console.log(mydiv.classList);  // DOMTokenList
          mydiv.classList.add("container");
          mydiv.classList.remove("container");

          使用classList,可以確保其他類名不受此次修改的影響,可以極大地減少類似的基本操作的復雜性;

          mydiv.classList.toggle("user");  // 切換user類
          // 確定元素中是否包含既定的類名
          if(mydiv.classList.contains("bd") && !mydiv.classList.contains("disabled")){
              // To Do 
          }
          // 迭代類名
          for(var i=0,len=mydiv.classList.length; i<len; i++){
              // To Do
              console.log(mydiv.classList[i]);
          }

          有了classList屬性,除非需要全部刪除所有類名,或者完全重寫元素的class屬性,否則也就用不到className了;

          需要確定的一點,classList這個DOMTokenList對象,它是實時的,如果className屬性改變了,它也會及時更新,同樣的,classList改變了,className屬性也會及時更新;

          IE9以下的瀏覽器不支持classList屬性,可以自定義一個CSSClassList類,模擬DOMTokenList對象的方法;

          function classList(e){
              // 以下兩行先注釋,否則后面的toArray默認調用的是DOMTokenList對象的的toArray,而它并不存在
              // 或者擴展內置的DOMTokenList的toArray
              // if(e.classList) return e.classList;  // 如果e.classList存在,則返回它
              // else return new CSSClassList(e);  // 否則,說偽造一個
              return new CSSClassList(e);
          }
          // CSSClassList是一個模擬DOMTokenList的對象
          function CSSClassList(e) { this.e = e;}
          // 如果e.className包含類名c則返回true,否則返回false
          CSSClassList.prototype.contains = function(c){
              // 檢查c是否是合法的類名
              if(c.length === 0 || c.indexOf(" ") != -1)
                  throw new Error("Invalid class name: '" + c + "'");
              // 首先是常規檢查
              var classes = this.e.className;
              if(!classes) return false;  // e不含類名
              if(classes === c) return true; // e有一個完全匹配的類名
              // 否則,把c自身看做一個單詞,利用正則表達式搜索c
              return classes.search("\\b" + c + "\\b") != -1;
          };
          // 如果c不存在,將c添加到e.className中
          CSSClassList.prototype.add = function(c){
              if(this.contains(c)) return;  // 如果存在,什么也不做
              var classes = this.e.className;
              if(classes && classes[classes.length - 1] != " ")
                  c = " " + c;  // 如果需要加一個空格
              this.e.className += c; // 將c添加到className中
          };
          // 將在e.className中出現的所有c都刪除
          CSSClassList.prototype.remove = function(c){
              if(c.length === 0 || c.indexOf(" ") != -1)
                  throw new Error("Invalid class name: '" + c + "'");
              // 將所有作為單詞的c和多余的尾隨空格全部刪除
              var pattern = new RegExp("\\b" + c + "\\b\\s*", "g");
              this.e.className = this.e.className.replace(pattern, "");
          };
          // 如果c不存在,將c添加到e.className中,并返回true
          // 否則,將e.className中出現的所有c都刪除,并返回false
          CSSClassList.prototype.toggle = function(c){
              if(this.contains(c)){ // 如果e.className包含類名c
                  thsi.remove();  // 刪除它
                  return false;
              }else{
                  this.add(c); // 添加
                  return true;
              }
          };
          // 返回e.className本身
          CSSClassList.prototype.toString = function(){
              return this.e.className;
          };
          // 返回在e.className中的類名
          CSSClassList.prototype.toArray = function(){
              return this.e.className.match(/\b\w+\b/g) || [];
          };
          // 應用
          var mydiv = document.getElementById("mydiv");
          var ccl = classList(mydiv);
          console.log(ccl);
          console.log(ccl.contains("newsdiv"));  // true
          ccl.add("topDiv");
          ccl.remove("newsdiv");
          ccl.toggle("newsdiv");
          console.log(ccl.toString());
          console.log(ccl.toArray());

          腳本化樣式表:

          在腳本化樣式表時,會使用到兩種類型的對象:

          第一類是元素對象,包括通過<link>元素包含的樣式表和在<style>元素中定義的樣式表,這兩個元素本身是常規的文檔元素,分別是由HTMLLinkElement和HTMLStyleElement類型表示,它們都可以修改元素的特性,如果它們有id,可以通過getEleementById()來獲取它們;

          // var mylink = document.getElementById("mylink");
          var mylink = document.getElementsByTagName("link")[0];
          console.log(mylink);
          var mystyle = document.getElementsByTagName("style")[0];
          console.log(mystyle);

          所有瀏覽器都會包含<style>元素和rel特性被設置為stylesheet的<link>元素引入的樣式表;

          第二類是CSSStyleSheet類型,表示樣式表本身;通過document.styleSheets屬性會返回一個只讀的類數組StyleSheetList對象集合,該集合具有length屬性和item()方法,集合內保存著CSSStyleSheet對象,表示與文檔關聯在一起的樣式表;

          var styleList = document.styleSheets;
          console.log(styleList);  // StyleSheetList
          console.log(styleList.length);  // 3
          console.log(styleList[0]);  // CSSStyleSheet
          // 遍歷
          var sheet = null;
          for(var i=0, len=document.styleSheets.length; i<len; i++){
              sheet = document.styleSheets[i];
              console.log(sheet.href);
          }

          可以直接通過HTMLLinkElement(<link>)或HTMLStyleElement(<style>)元素的sheet屬性獲取CSSStyleSheet對象;低版本的IE不支持sheet,但提供了一個同樣作用的styleSheet屬性;

          var mylink = document.getElementById("mylink");
          var mylink = document.getElementsByTagName("link")[0];
          console.log(mylink.sheet);  // CSSStyleSheet
          console.log(mylink.styleSheet); // 在低版本的IE中返回CSSStyleSheet
          var mystyle = document.getElementsByTagName("style")[0];
          console.log(mystyle.sheet);  // CSSStyleSheet
          console.log(mystyle.styleSheet);  // 在低版本的IE中返回CSSStyleSheet

          兼容處理:

          function getStyleSheet(element){
              return element.sheet || element.styleSheet;
          }
          var link = document.getElementsByTagName("link")[0];
          var sheet = getStyleSheet(link);
          console.log(sheet.href);

          在使用之前,檢測瀏覽器是否支持DOM2級樣式表;

          var supportsDOM2StyleSheet = document.implementation.hasFeature("StyleSheets","2.0");

          CSSStyleSheet對象:

          接口代表一個 CSS 樣式表,并允許檢查和編輯樣式表中的規則列表;其繼承自StyleSheet接口,后者可以作為一個基礎接口來定義非CSS樣式表;從接口繼承的屬性如下:

          disabled:表示樣式表是否被禁用的布爾值,R/W,將這個值設置為true可以禁用樣式表;

          var styleList = document.styleSheets;
          var ss = styleList[2];
          console.log(ss.disabled);
          ss.disabled = true;  // 元素的樣式失效
          // 封裝一個小函數,用來開啟或關閉樣式表
          // 如果傳遞一個數字,將其當做document.styleSheets對象中一個索引
          // 如果傳遞一個字符串,將其當作CSS選擇器并傳遞給document.querySelectorAll()
          function disableStyleSheet(ss){
              if(typeof ss === "number")
                  document.styleSheets[ss].disabled = true;
              else{
                  var sheets = document.querySelectorAll(ss);
                  for(var i=0; i<sheets.length; i++)
                      sheets[i].disabled = true;
              }
          }
          disableStyleSheet(0);
          disableStyleSheet("style");
          disableStyleSheet("link");

          href:如果樣式表是通過<link>包含的,則是樣式表的URL,否則null;

          media:當前樣式表支持的所有媒體類型的MediaList類數組集合;與所有DOM集合一樣,這個集合也有一個length屬性和一個item()方法;如果集合為空,表示樣式表適用于所有媒體;

          <style media="screen and (min-width: 500px),tv and (max-width: 1000px)">
              .container{width:300px;height:200px;background-color: salmon;}
          </style>
          <script>
          var styleList = document.styleSheets;
          var ss = styleList[2];
          console.log(ss.media);  // MediaList
          console.log(ss.media.length); // 2
          console.log(ss.media[0]);  // screen and (min-width:500px)
          console.log(ss.media.item(1)); // tv and (max-width:1000px)

          MediaList對象還擁有mediaText屬性,返回元素media特性的值;

          console.log(ss.media.mediaText);

          MediaList對象擁有appendMedium(medium)和deleteMedium()方法,分別用作添加和刪除媒介;

          ss.media.appendMedium("print");
          ss.media.deleteMedium("tv and (max-width:1000px)");

          一般來說,很少去操作media屬性;

          ownerNode:指向擁有當前樣式表的節點的指針,樣式表可能是在HTML中通過<link>或<style>引入的,如果樣式表是其他樣式表是通過@import導入的,該屬性值為null,低版本IE不支持該屬性;

          console.log(styleList[0].ownerNode); // link
          console.log(styleList[1].ownerNode); // style

          parentStyleSheet:在當前樣式表是通過@import導入的情況下,該屬性是一個指向導入它的樣式表的指針;

          // styleList[1]獲取的是一個<style>其中使用@import導入一個CSS文件
          console.log(styleList[1].cssRules[0]); 
          console.log(styleList[1].cssRules[0].parentStyleSheet);  // CSSStyleSheet

          title:ownerNode中title屬性的值;

          type:表示樣式表類型的字符串,對CSS樣式表而言,是”text/css”;

          注:除了disabled屬性之外,其他屬性都是只讀的;

          操作樣式表規則:

          除了以上所有這些屬性,CSSStyleSheet類型還定義了用來查詢、插入和刪除樣式表規則的API;

          cssRules:返回樣式表中包含的所有樣式規則的CSSRuleList類型的實時集合,該集合中的元素為CSSStyleRule對象;低版本的IE不支持,但有個類似的rules屬性;

          var ss = styleList[1];
          console.log(ss);
          console.log(ss.cssRules);
          console.log(ss.rules);  // 是IE專用
          console.log(ss.cssRules[0]);  // CSSStyleRule

          ownerRule:如果樣式表是通過@import導入的,該屬性就是一個指針,返回表示導入的規則的CSSImportRule對象,否則為null,低版本的IE不支持;

          insertRule(rule, index):創建(插入)規則,向CSSRules集合中指定的位置插入rule字符串,該方法接受兩個參數:規則文本和表示在哪里插入規則的索引;

          var sheet = document.styleSheets[1];
          sheet.insertRule("body{background-color:silver}",0);
          console.log(sheet.cssRules);


          低版本的IE支持一個類似的addRule(selector, style, index)方法,接受兩個必選和一個可選參數:選擇器和CSS樣式信息,一個可選參數:插入規則的位置;

          document.styleSheets[1].addRule("h1","font-size:3em;color:red;",2);
          console.log(document.styleSheets[1].cssRules);

          這個方法所有瀏覽器也都支持;

          跨瀏覽器方式:

          var sheet = document.styleSheets[0];
          function insertRule(sheet, selectorText, cssText, position){
              if(sheet.insertRule){
                  sheet.insertRule(selectorText + "{" + cssText + "}", position);
              }else{
                  sheet.addRule(selectorText, cssText, position);
              }
          }
          insertRule(sheet, "body", "background-color:silver", 0);

          deleteRule(index):刪除cssRules集合(樣式表)中指定位置的規則;低版本的IE不支持,但支持一個類似的removeRule()方法,這個方法所有瀏覽器也都支持;

          document.styleSheets[1].deleteRule(0);
          document.styleSheets[1].removeRule(0);
          console.log(document.styleSheets[1].cssRules);

          跨瀏覽器方式:

          var sheet = document.styleSheets[0];
          function deleteRule(sheet, index){
              if(sheet.deleteRule){
                  sheet.deleteRule(index);
              }else{
                  sheet.removeRule(index);
              }
          }
          deleteRule(sheet,0);

          CSSStyleRule規則對象:

          CSSStyleRule對象表示樣式表中的每一條規則;繼承自CSSRule接口,實際上CSSRule是專供其他類型繼承的基類,其中最常見的是CSSStyleRule類型,表示樣式信息;

          var sheet = document.styleSheets[2];
          var rules = sheet.cssRules || sheet.rules;
          var rule = rules[0];
          console.log(rule); // CSSStyleRule

          CSSRule接口屬性:

          • cssText:返回整條規則對應的文本;低版本的IE不支持
          • parentRule:只讀,如果當前規則是導入的規則,這個屬性引用的就是導入規則,否則為null;或此規則是 @media 塊中的樣式規則, 則其父規則將是該 CSSMediaRule;IE不支持
          • parentStyleSheet:當前規則所屬的樣式表,低版本的IE不支持;
          • type:表示規則類型的常量值,對于樣式規則,值為1;IE不支持;

          這些常量被定義在CSSRule接口中,值為:

          常量值接口

          • CSSRule.STYLE_RULE1CSSStyleRule
          • CSSRule.IMPORT_RULE3CSSImportRule
          • CSSRule.MEDIA_RULE4CSSMediaRule
          • CSSRule.FONT_FACE_RULE5CSSFontFaceRule
          • CSSRule.PAGE_RULE6CSSPageRule
          • CSSRule.KEYFRAMES_RULE7CSSKeyframesRule
          • CSSRule.KEYFRAME_RULE8CSSKeyframeRule
          • 9 保留供將來使用
          • CSSRule.NAMESPACE_RULE10CSSNamespaceRule
          • CSSRule.COUNTER_STYLE_RULE11CSSCounterStyleRule
          • CSSRule.SUPPORTS_RULE12CSSSupportsRule
          • CSSRule.DOCUMENT_RULE13CSSDocumentRule
          • CSSRule.FONT_FEATURE_VALUES_RULE14
          • CSSRule.VIEWPORT_RULE15CSSViewportRule
          • CSSRule.REGION_STYLE_RULE16CSSRegionStyleRule
          • CSSRule.UNKNOWN_RULE0CSSUnknownRule
          • CSSRule.CHARSET_RULE2CSSCharsetRule

          CSSStyleRule對象屬性:

          • selectorText:只讀,返回當前規則的選擇器;
          • style:只讀,返回一個CSSStyleDeclaration對象,可以通過它設置和取得規則中特定的樣式值;
          • styleMap:一個StylePropertyMap對象;
          console.log(rule.cssText);  // 定義規則的字符串
          console.log(rule.parentRule);
          console.log(rule.parentStyleSheet); // CSSStyleSheet
          console.log(rule.selectorText);  // 選擇器
          console.log(rule.style);  // CSSStyleDeclaration
          console.log(rule.styleMap);  // StylePropertyMap
          console.log(rule.type);  // 1

          最常用的是cssText、selectorText和style這三個屬性;cssText屬性與style.cssText屬性類似,但并不相同,前者包含選擇器和圍繞樣式信息的花括號,后者只包含樣式信息;cssText是只讀的,style.cssText是可寫的;

          console.log(rule.cssText);  // .container{color:white}
          console.log(rule.style.cssText); // color:white
          rule.style.cssText = "background-color:purple";
          console.log(rule.cssText);  // .container{background-color:purple;}
          console.log(rule.style.cssText); // background-color:purple
          rule.style.cssText += "color:white;";

          大多數情況下,僅使用style屬性就可以滿足所有操作樣式規則的需求了;這個對象就像每個元素上的style屬性一樣,可以通過它讀取和修改規則中的樣式信息;

          console.log(rule.style.width);
          console.log(rule.style.height);
          rule.style.backgroundColor = "lightgray";
          console.log(rule.style.cssText);

          CSSStyleRule對象的style屬性,使用方式和內聯style對象的使用方式是一致的,但要注意,一個是規則對象的style屬性對象,一個是內聯style對象;

          // 遍歷樣式表的規則
          var ss = document.styleSheets[0];  // 第一個樣式表
          var rules = ss.cssRules ? ss.cssRules : ss.rules;  // 樣式表規則
          for(var i=0; i<rules.length; i++){
              var rule = rules[i];
              if(!rule.selectorText) continue;  // 跳過@import和非樣式規則
              var selector = rule.selectorText;  // 選擇器
              var ruleText = rule.style.cssText; // 文本形式的樣式
              // 如果規則應用在h1元素上,也將其應用到h2元素上
              // 注意,僅當選擇器在字面上為h1時這才起作用
              if(selector == "h1"){
                  if(ss.insertRule)
                      ss.insertRule("h2 {" + ruleText + "}", rules.length);
                  else if(ss.addRule)
                      ss.addRule("h2", ruleText, rules.length);
              }
              // 如果規則設置了text-decoration屬性,則將其刪除
              if(rule.style.textDecoration){
                  if(ss.deleteRule)
                      ss.deleteRule(i);
                  else if(ss.removeRule)
                      ss.removeRule(i);
                      i--; // 調整循環索引,因為以上的規則i+1現在即為規則i
              }
          }

          創建新樣式表:

          可以創建一個新樣式表并將其添加到文檔中;使用DOM技術,創建一個<style>元素,并將其插入到文檔的頭部,然后再用其innerHTML屬性來設置樣式表內容;在低版本的IE中,CSSStyleSheet對象通過非標準方法document.createStyleSheet()來創建,其樣式文本用cssText屬性值為指定;

          // 創建一個新樣式表
          // 對文檔添加一個樣式表,用指定的樣式填充它
          // style參數可能是字符串或對象,如果它是字符串,就把它作為樣式表的文本
          // 如果它是對象,將每個定義樣式規則的每個屬性添加到樣式表中
          // 屬性名即為選擇器,其值即為對應的樣式
          function addStyles(styles){
              var styleElt, styleeSheet;  // 先創建一個新樣式表
              if(document.createStyleSheet)  // 如果是IE
                  styleSheet = document.createStyleSheet();
              else{
                  var head = document.getElementsByTagName("head")[0];
                  styleElt = document.createElement("style"); // 新的<style>元素
                  head.appendChild(styleElt);
                  // 這個新樣式表應該是最后一個
                  styleSheet = document.styleSheets[document.styleSheets.length - 1];
              }
              // 向其中插入樣式
              if(typeof styles === "string"){
                  if(styleElt)
                      styleElt.innerHTML = styles;
                  else
                      styleSheet.cssText = styles;  // IE
              }else{  // 參數是規則對象
                  var i = 0;
                  for(selector in styles){
                      if(styleSheet.insertRule){
                          var rule = selector + "{" + styles[selector] + "}";
                          styleSheet.insertRule(rule, i++);
                      }else{
                          styleSheet.addRule(selector, styles[selector], i++);
                      }
                  }
              }
          }
          // 應用
          var styles = "h2 {font-size: 2em; color: red;}";
          addStyles(styles);
          var rule = document.styleSheets[1].cssRules[0];
          addStyles(rule);

          CSS動畫:腳本化CSS的最常見的用途之一就是產生視覺動畫效果;其原理是使用setTimeout()或setInterval()重復調用函數來修改元素的內聯樣式,以達到產生視覺差的動畫效果;


          主站蜘蛛池模板: 在线视频国产一区| 日本伊人精品一区二区三区| 乱码人妻一区二区三区| 国产乱码精品一区二区三区四川人 | 日韩aⅴ人妻无码一区二区| 无码av免费一区二区三区| 国产成人无码精品一区二区三区| 精品伦精品一区二区三区视频 | 99久久精品日本一区二区免费 | 国产区精品一区二区不卡中文| 中文字幕日韩一区二区不卡| 日本亚洲国产一区二区三区| 日本无码一区二区三区白峰美| 中文字幕一区二区三| 无人码一区二区三区视频| 亚洲日韩AV无码一区二区三区人| 2020天堂中文字幕一区在线观 | 精品乱码一区二区三区四区| 午夜DV内射一区二区| 亚洲国产综合无码一区| 国产精品视频一区二区猎奇| 无码人妻精品一区二区蜜桃网站 | 97se色综合一区二区二区| 精品久久久久一区二区三区| 国产高清在线精品一区二区| 久久久一区二区三区| 一区二区视频免费观看| 亚洲一区二区三区久久| 国产主播一区二区| 无码人妻精品一区二区三区99不卡| 亚洲av无码不卡一区二区三区| 天堂一区二区三区在线观看| 久久国产精品亚洲一区二区| 精品无码人妻一区二区三区18| 亚洲一区二区精品视频| 精品亚洲一区二区三区在线播放| 久久se精品一区精品二区| 日韩人妻无码一区二区三区久久| 亚洲综合av一区二区三区不卡| 天堂va视频一区二区| 亚洲AV成人精品日韩一区18p|