02--JS02--高階

Edmond辉仔發表於2024-05-14

JavaScript02: 進階

一. 變數宣告

1.1 變數提升

// 以下程式碼,或多或少會有些問題的
function fn(){
    console.log(name);
    var name = '大馬猴';
}

fn()


// 問題: 
name變數先使用,再定義   這麼寫程式碼,在其他語言裡. 絕對是不允許的
但是在js裡,不但允許,還能執行,為什麼呢? 
因為在js執行的時候,它會首先檢測整體程式碼,發現在程式碼中會有name使用
則執行時,就會自動變成這樣的邏輯:

// 變數提升的邏輯:
function fn(){
    var name;
    console.log(name);
    name = '大馬猴';
}
fn()
console.log(a);  // undefined

// 看到了麼,實際執行的時候和寫程式碼的順序可能會不一樣
這種 把變數 提升到程式碼塊第一部分執行  的邏輯,被稱為變數提升

1.2 let 宣告變數

結論一:用let宣告變數,是新版本javascript ES6 提倡的一種宣告變數的方案

// ES6提出 用let 宣告變數,防止變數提升的邏輯

function fn(){
    console.log(name);  // 直接報錯, let變數不可以變數提升
    let name = '大馬猴'; 
}
fn()

結論二:在同一個作用域內. let宣告的變數只能宣告一次,其他使用上和var沒有差別

function fn(){
    // console.log(name);  // 直接報錯, let變數不可以變數提升.
    // let name = '大馬猴';
    var name = "周杰倫";
    var name = "王力宏";   // 不會報錯
    console.log(name);
}

fn()

// var本意是宣告變數,同一個變數,被宣告兩次(都是函式體內部,區域性變數),顯然也是不合理的

ES6規定:let宣告的變數,在同一個作用域內,只能宣告一次

function fn(){
    // console.log(name);  // 直接報錯, let變數不可以變數提升.
    // let name = '大馬猴';
    let name = "周杰倫";
    console.log(name);
    
    let name = "王力宏";    // 報錯,同一個作用域 let 宣告同一個變數,只能一次
    console.log(name);
}
fn()


// 注意: 報錯是發生在程式碼檢查階段. 所以上述程式碼,根本就執行不了

二. 閉包函式

先看一段程式碼

let name = "周杰倫"
function chi(){
    name = "吃掉"
}
chi();
console.log(name);  // "吃掉"


// 發現沒有: 在函式內部修改,外部的變數是十分容易的一件事. 尤其是全域性變數. 這是非常危險的. 
// 試想 寫了一個函式. 要用到name, 結果被別人寫的某個函式給修改掉了...

接下來,看一個案例:

準備兩個工具人. 來編寫程式碼. 分別是js01和js02.

// 1號工具人
var name = "alex"

// 定時器,5000秒後執行
setTimeout(function(){
    console.log("一號工具人:" + name)   // 一號工具人還以為是alex呢, 但是該變數是不安全的.
}, 5000);
// 2號工具人
var name = "周杰倫"
console.log("二號工具人", name);

html:

<script src="js01.js"></script>

<script src="js02.js"></script>

此時執行的結果:

很明顯, 雖然各自js在編寫時是分開的. 但是在執行時, 是在同一個空間內執行的. 他們擁有相同的作用域

此時的變數勢必是非常非常不安全的. 那麼如何來解決呢?

注意:在js裡 變數是有作用域的. 也就是說一個變數的宣告和使用是有範圍的,不是無限的

// 驗證: 變數是有作用域的
function fn(){
    let love = "愛呀"
}
fn()

console.log(love)

// 直接就報錯了   也就是說. 在js裡 變數作用域是有全域性和區域性  的概念 

直接宣告在最外層的變數,就是全域性變數。所有函式、所有程式碼塊都可以共享的

在函式內和程式碼塊內宣告的變數,尤其是函式內,宣告出來的變數它是一個區域性變數,外界是無法進行訪問的

我們就可以利用這一點,來給每個工具人建立一個區域性空間. 就像這樣:

// 1號工具人   都是自執行函式
(function(){
    var name = "alex";
    setTimeout(function(){
        console.log("一號工具人:"+name)
    }, 5000);
})()
// 二號工具人  都是自執行函式
!function(){
    var name = "周杰倫"
    console.log("二號工具人", name);
}()

執行結果

這樣,雖然解決了變數的衝突問題

但是想想. 如果在外面,需要函式內部的一些東西,來進行相關操作,怎麼辦?

比如 一號工具人要提供一個功能(加密),外界要呼叫, 怎麼辦?

// 1號工具人

// 區域性函式中,對外介面 方式一: return 返回到 全域性變數中   ===> 閉包函式
// 1.首先:全域性要使用,那js檔案中,就不能是自執行函式,要設定一個名字
let jiami = (function(){
    let key = "10086" // 假裝我是秘鑰
    
    // 我是一個加密函式
    let mi = function(data){  // 資料
        console.log("接下來, 我要加密了,rsa哦. 很厲害的")
        console.log("秘鑰:"+key);
        console.log("資料:"+data);
        // 返回密文
        return "我要返回密文";
    }
    
    // 2.其次:外面可能需要用到該功能. 故 需要該變數返回(暴露到全域性空間). 返回加密函式
    return mi;
})();



// 區域性函式中,對外介面 方式二:藉助於window物件,將返回的變數 直接 賦值到全域性變數中
(function(){
    let key = "10086" // 假裝我是秘鑰
    
    // 我是一個加密函式
    let mi = function(data){  // 資料
        console.log("接下來, 我要加密了,rsa哦. 很厲害的")
        console.log("秘鑰:"+key);
        console.log("資料:"+data);
        // 返回密文
        return "我要返回密文";
    }
    
    // 對外的介面
    window.mi = mi;
})();

注意:如果封裝一個加密js包的時候,就還得準備出解密的功能

並且, 不可能一個js包就一個功能吧。 那也太痛苦了(js檔案起名字),那怎麼辦?

可以返回一個物件,物件裡面可以存放好多個功能

而一些不希望外界觸碰的功能. 就可以很好的保護起來.

// 1號工具人.
let jiami = (function(){

    let key = "10086" // 加裝我是秘鑰
   
	// 該函式只屬於該模組內部,外界無法訪問.  就不返回
    let n = {
        abc:function(){
            console.log("我是abc. 你叫我幹什麼?")
        }
    }

    
    // 外面需要用到的功能,就進行返回.
    return {
        rsa_jiami: function(data){
            console.log("接下來, 我要加密了,rsa哦. 很厲害的")
            console.log("秘鑰:"+this.get_rsa_key() + key);
            n.abc();
            console.log("資料:"+data);
            return "我要返回密文";
        },
        aes_jiami: function(data){
            console.log("接下來, 我要加密了,aes哦. 很厲害的")
            console.log("秘鑰:"+this.get_aes_key());
            n.abc();
            console.log("秘鑰:"+key);
            console.log("資料:"+data);
            return "我要返回密文";
        },
        get_rsa_key: function() {
            return this.rsa_key = "rsa的key", this.rsa_key
        },
        get_aes_key: function() {
            return this.rsa_key = "aes的key", this.rsa_key
        }
    }
})();

html裡面使用時:

<script>
    miwen = jiami.rsa_jiami("吃你的糖葫蘆吧");
    console.log(miwen);
</script>

OK. 至此. 何為閉包? 上面這個就是閉包

相信你百度一下就會知道,什麼內層函式使用外層函式變數、什麼讓一個變數常駐記憶體等等

其實細看,它之所以稱之為閉包,它是一個封閉的環境。在內部. 自己和自己玩兒

避免了對該模組內部的衝擊和改動. 避免的變數之間的衝突問題

閉包的特點:

  1. 內層函式對外層函式變數的使用
  2. 會讓變數常駐於記憶體

這倆玩意就不解釋了, 和python的閉包是一個意思。不懂沒關係,能看懂它的執行過程就好

三. JS中的各種操作(非互動)

3.1 定時器

在JS中, 有兩種設定定時器的方案

// 延時器:經過xxx時間後, 執行xxx函式
t = setTimeout(函式, 時間)


// 5000毫秒  5秒後列印我愛你
t = setTimeout(function(){
    console.log("我愛你")
}, 5000); 


clearTimeout(t)  // 停止一個定時器
// window.clearTimeout(t)  
// 定時器:每隔 xxx時間, 執行一次xxx函式
t = setInterval(函式, 時間) 


// 每隔5秒鐘, 列印`我愛你`
t = setInterval(function(){
    console.log("我愛你")
}, 5000)

window.clearInterval(t)  // 停止一個定時器

for(let i = 0; i <= 9999; i++)window.clearInterval(i);  // 清理掉所有定時器
// 定時器 關於js逆向,常遇到的:


// 1.心跳檢測     一般是用來監測使用者 是否掉線了,也可以用來監測使用者瀏覽器環境是否健康
// 這個就是  服務端 主動向 瀏覽器端 傳送請求檢測
http是被動響應的  伺服器端只能不停的間隔傳送檢測請求,才能時刻監測客戶端某個元素的狀態(是否點選 或 掃描二維碼)等

js逆向中:心跳檢測
就是正常瀏覽器是 不停的心跳檢測 且有資料返回,
js逆向程式碼時,可能只發一次,服務端就給做反爬 禁IP什麼的


// 2.無限debugger
// 無限debugger的核心
setInterval(function(){
    debugger;   // 設定斷點
}, 1000)

網頁頁面中,會正常顯示其他的html程式碼,但是一旦F12除錯,就會一直處理斷點中


// 解決原理:
在source原始碼中,setInderval 這一行(沒進入定時器體內之前),左鍵點選行號(設定斷點)

重新整理頁面,頁面會除錯暫停到 設定斷點這一行

再在控制檯(console)中,將定時器幹掉 (重置定時器為普通的空函式 setInterval=function(){}; )

3.2 關於時間

eg:http://www.baidu.com/s?word=jfdsaf&t=1640090719637 引數t就是時間戳

var d = new Date(); // 獲取當前系統時間
var d = new Date("2018-12-01 15:32:48");  // YYYY-MM-DD HH:mm:ss得到一個具體時間

// 時間格式化
year = d.getFullYear();   // 年份
month = d.getMonth() + 1; // 月份. 注意月份從0開始
date = d.getDate();       // 日期
hour = d.getHours();      // 小時
minute = d.getMinutes();  // 分鐘
seconds = d.getSeconds(); // 秒

format_date = year + "-" + month + "-" + date + " " + hour + ":" + minute + ":" + seconds;



// 時間戳     表示從1970-1-1 00:00:00 到現在一共經過了多少毫秒
var d = new Date(); 
console.log(d.getTime())


// 注意1:python的時間戳 單位是秒
import time
print( int(time.time() * 1000) )  # 擴大一千倍,再取整


// 注意2: 有些時候,前端例項化日期物件時,會少一個呼叫括號
var d = new Date;     // 坑人寫法,也可以
console.log(d.getTime())

3.3 eval函式(必須會)

http://tools.jb51.net/password/evalencode 一個線上JS處理eval的網站. 大多數的eval加密. 都可以搞定了.

// eval:
可以動態的把字串,當成js程式碼執行   // 從功能上講非常簡單,和python裡面的eval是一樣的

s = "console.log('我愛你')";
eval(s);


// 重點:eval加密
擴充使用 --> 前端利用eval的特性來完成反爬操作

// 解決核心:
eval函式,裡面傳遞的應該是  即將要執行的 程式碼(字串)



// eg:
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('0.1(\'我愛你\')',62,2,'console|log'.split('|'),0,{}))


// 記住eval()裡面是字串  !!!
這一坨看起來, 肯定很不爽. 怎麼變成看著很舒服的樣子呢?  想看看這個字串長什麼樣?  
就把eval()裡面的東西複製出來
執行一下  // 注意:一般是個自執行函式,直接貼上會報錯  故加個括號 eg: (eval中的程式碼) 
         //  或 和下面截圖一樣  賦值給變數,再列印變數
最終一定會得到一個字串,要不然eval()執行不了的

3.4 prototype 原型物件

prototype 原型物件,作用是 給類增加功能擴充套件 的一種模式

寫個物件導向來看看.

function People(name, age){
    this.name = name;
    this.age = age;
    this.run = function(){
        console.log(this.name+"在跑")
    }
}

p1 = new People("張三", 18);
p2 = new People("李四", 19);

p1.run();
p2.run();

現在程式碼寫完了. 突然之間, 感覺好像少了個功能. 人不應該就一個功能. 光會吃是不夠的. 還得能夠ooxx. 怎麼辦?

直接改程式碼? 可以,但不夠好. 如果這個類不是我寫的呢? 隨便改別人程式碼是很不禮貌的,也很容易出錯. 怎麼辦?

可以在我們自己程式碼中,對某個型別動態增加功能。此時就用到了prototype

function People(name, age){
    this.name = name;
    this.age = age;
    this.run = function(){
        console.log(this.name+"在跑")
    }
}

// 透過prototype,可以給People增加功能  屬性或方法
People.prototype.xxoo = function(){
    console.log(this.name+"還可以xxoo");
}

p1 = new People("張三", 18);
p2 = new People("李四", 19);

p1.run();
p2.run();

p1.xxoo();
p2.xxoo();

幾個重要概念

3.4.1 構造器

構造一個物件的函式. 叫構造器.

function People(){       // People 就是構造器 constractor
    
}

var p = new People();    // 呼叫構造器 ---> 物件

p.constractor == People; // true

3.4.2 原型物件

每一個js物件中,都有一個隱藏屬性__proto__,指向該物件的 原型物件

在執行該物件的方法或者查詢屬性時,首先, 物件自己(構造器中宣告的)是否存在該屬性或者方法

如果存在, 就執行自己的. 如果自己不存在. 就去找 原型物件

function Friend(){
    this.chi = function(){
        console.log("我的朋友在吃");
    }
};

// 指定Friend 的原型物件  
Friend.prototype = {
    chi: function(){
        console.log("我的原型在吃")
    }
};

// 或者這種寫法
Friend.prototype.chi = function(){
    console.log("我的原型在吃")
};


f = new Friend();
f.chi();   // 執行結果: 我的朋友在吃


// 屬性查詢順序:
先查詢該物件(構造器中宣告的)中  是否有chi這個方法
再找,它的原型物件上 是否有chi這個方法





// 總結: !!!
Friend    //  構造器

f         //  物件

f.__proto__  <===> Friend.prototype  // 構造器的prototype屬性 和 物件的 __proto__,都是指向f物件 的 原型物件

3.4.3 原型鏈

原型鏈(prototype chain):是屬性查詢的方式

當呼叫一個物件的屬性時,如果物件沒有該屬性,從物件的原型物件上去找該屬性,

如果原型上也沒有該屬性,那就去找原型的原型,直到最後返回null為止,null沒有原型。

// 前提:
每個物件身體裡. 都隱藏著 __proto__屬性 也就是它的 原型物件
同時 原型物件 也是物件, 也就是說 原型物件 也有  __proto__ 屬性


類似於.....這樣:

f.__proto__.__proto__  ===> Friend.prototype.__proto__ ===>  Object.prototype // Object物件的原型

列印出來的效果是這樣的:

// 故:在執行 f.toString() 的時候不會報錯. 可以正常執行的原因,就在這裡

執行過程:  
先找 f物件 中是否有 toString 沒有
找它的 原型物件,原型物件 中沒有
繼續找 原型物件的原型物件
直至找到Object的原型為止,如果還沒有,就報錯了. 


f.hahahahahahah()  // 報錯 

// 綜上:
原型鏈是js 方法查詢的路徑指示標

3.4.4 原型鏈的延伸使用

用原型鏈能做什麼? 網站反爬(噁心): 有些頁面網站,透過原型鏈,讓你F12時,一直無限debug

看一段神奇的程式碼

(function(){debugger})();   // 這樣一段程式碼可以看到. 瀏覽器進入了debugger斷點.  


// 這段程式碼的背後是什麼呢? 

// 注意:
在js程式碼執行時,每一個function的物件,都是透過Function()來建立的 
也就是說 函式是Function()的物件

// 校驗:
function fn(){}
console.log(fn.__proto__.constructor);  // fn函式的原型 構造器是 ƒ Function() { [native code] }



// 所以:函式就是Function的物件. 那麼,我們也可以透過Function來構建一個函式. 

new Function('debugger')();  // 效果一樣的. 

OK. 這東西對我們來說有什麼用. 上程式碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="haha.js"></script>
  <script>
    txsdefwsw();
  </script>
</head>
<body>
    有內鬼. 終止交易
</body>
</html>

haha.js 中的內容如下:

function txsdefwsw() {
    var r = "V", n = "5", e = "8";

    function o(r) {
        if (!r) return "";
        for (var t = "", n = 44106, e = 0; e < r.length; e++) {
            var o = r.charCodeAt(e) ^ n;
            n = n * e % 256 + 2333, t += String.fromCharCode(o)
        }
        return t
    }

    try {
        var a = ["r", o("갯"), "g", o("갭"), function (t) {
            if (!t) return "";
            for (var o = "", a = r + n + e + "7", c = 45860, f = 0; f < t.length; f++) {
                var i = t.charCodeAt(f);
                c = (c + 1) % a.length, i ^= a.charCodeAt(c), o += String.fromCharCode(i)
            }
            return o
        }("@"), "b", "e", "d"].reverse().join("");
        !function c(r) {
            (1 !== ("" + r / r).length || 0 === r) && function () {
            }.constructor(a)(), c(++r)
        }(0)
    } catch (a) {
        setTimeout(txsdefwsw, 100);
    }
}

結果:頁面跑起來沒什麼問題. 但是會無限debugger;

解決方案:

  1. 找到斷點出. 右鍵 -> never pause here

  2. 寫js hook程式碼

    var xxxx = Function.prototype.constructor;
    Function.prototype.constructor = function(code){
        console.log("i love you");
        if (code != 'debugger'){
            return new xxxx(code);
        } else {
            return;
        }
    }
    

    更加詳細的hook. 下節課會講.

3.5 window物件

window物件是一個很神奇的東西,可以理解成javascript的全域性,是整個瀏覽器的全域性作用域

如果預設不用任何東西,訪問一個識別符號,那麼預設是在用window物件

例如:

eval === window.eval    // true
setInterval === window.setInterval  // true

var a = 10; 
a === window.a    // true

function fn(){}
fn === window.fn  // true

window.mm = "愛你"

console.log(mm); //"愛你"



// window 中有很多功能物件,還可以控制頁面跳轉

window.location.herf = "新地址"  // 當前視窗 跳轉到新地址url

綜上:全域性變數可以用window.xxx來表示

ok. 接下來注意看. 我要搞事情了

(function(){
    let chi = function(){
        console.log("我是吃")
    }
    window.chi = chi
})();

chi()


// 換一種寫法. 你還認識麼?
(function(w){
    let chi = function(){
        console.log("我是吃")
    }
    w.chi = chi
})(window);



// 再複雜一點
(function(w){
    let tools = {
        b64: function(){
            console.log("我是計算B64");
            return "b64";
        },
        md5: function(){
            console.log("我是計算MD5");
            return "MD5"
        }
    }
    w.jiami = {
        AES: function(msg){
            return tools.b64(),
                tools.md5(),
                'god like';
        },
        DES: function(){
            console.log("我是DES");
        },
        RSA: function(){
            console.log("我是RSA");
        }
    }
})(window);

jiami.AES("吃了麼");

3.6 call和apply

對於逆向工程師而言,並不需要深入的理解call和apply的本質作用.,只需要知道這玩意執行起來的邏輯順序是什麼即可

在執行時,正常的js呼叫:

function People(name, age){
    this.name = name;
    this.age = age;
    this.chi = function(){
        console.log(this.name, "在吃東西")
    }
}

p1 = new People("alex", 18);
p2 = new People("wusir", 20);

p1.chi();
p2.chi();

接下來,可以使用call和apply也完成同樣的函式呼叫

function People(name, age){
    this.name = name;
    this.age = age;
    this.chi = function(what_1, what_2){
        console.log(this.name, "在吃", what_1, what_2);
    }
}

p1 = new People("alex", 18);
p2 = new People("wusir", 20);

p1.chi("饅頭", "大餅");
p2.chi("大米飯", "金坷垃");


function eat(what_1, what_2){
    console.log(this.name, "在吃", what_1, what_2);
}

// call的語法是: 函式.call(物件, 引數1, 引數2, 引數3....)
// 執行邏輯是: 執行函式. 並把物件傳遞給函式中的this.  其他引數照常傳遞給函式
eat.call(p1, "查克拉", "元宇宙");

apply和他幾乎一模一樣. 區別是: apply傳遞引數要求是一個陣列

eat.apply(p1, ["苞米茬子", "大餅子"]);

3.7 ES6中的箭頭函式

在ES6中簡化了函式的宣告語法.

var fn = function(){};
var fn = () => {};

var fn = function(name){}
var fn = name => {}
var fn = (name) => {}

var fn = function(name, age){}
var fn = (name, age) => {}

3.8 ES6中的promise(難)

具體執行過程和推理過程. 請看影片. 這裡很饒騰.

function send(url){
    return new Promise(function(resolve, reject){
        console.log("我要傳送ajax了", url)
        setTimeout(function(){
            console.log("我傳送ajax回來了")
            // 成功了, 要去處理返回值
            resolve("資料", url);
        }, 3000);
    });
}

send("www.baidu.com").then(function(data){
    console.log("我要處理資料了啊", data);
    return send("www.google.com");
}).then(function(data, url){
    console.log("我又來處理資料了", data);
});

3.9 逗號運算子

function s(){
    console.log(1), console.log(2), console.log(3);  // 從前向後執行 ,1,2,3
    let s = (1, 2, 3); // 整體進行賦值的時候. 取的是最後一個值 3
    console.log(s);
    // 注意. 這個括號可以在返回值時省略
    var a;
    return a=10,
    a++,
    a+=100,
    {name:"alex", "a":a};
}
let r = s();
console.log(r);  // {name: 'alex', a: 111}

3.10 三元運算子

// 三元運算子
條件?值1:值2      // 條件成立時,返回 ?後面的    反之,返回 :後面的

let a = 10;
let b = 20;
let d = a > b? a: b ;    
console.log(d);  // 20 


看一個噁心的:
let a = 10;
let b = 20;
let d = 17;
let c = 5;

let e;
let m;

e = (e = a > 3 ? b : c, m = e < b++ ? c-- : a = 3 > b % d ? 27: 37, m++)
console.log(e);
console.log(c);
console.log(m);

3.11 JS hook

hook又稱鉤子,可以在呼叫系統函式之前,先執行我們的函式. 鉤子函式

例如:hook eval

eval_ = eval; // 先儲存系統的eval函式
eval = function(s){
    console.log(s);
    debugger;
    return eval_(s);
}
eval()
eval.toString = function(){return 'function eval() { [native code] }'}  // 可能會被檢測到, 用這種方案來進行

對Function的hook, 主要為了解決無限debugger

fnc_ = Function.prototype.constructor;
Function.prototype.constructor = function(){
    if(arguments[0]==='debugger'){
        return;
    } else {
        return fnc_.apply(this, arguments);
    }
}

上面都是hook的系統函式. 但有時,需要hook某個屬性. 此時應該怎麼辦?

var v;
Object.defineProperty(document, "cookie", {
    set: function(val) {
        console.log("有人來存cookie了");
    	v = val;
        debugger;
        return val;
    },
    get() {
        console.log("有人提取cookie了");
        debugger;
        return v;
    }
});

剩下的,就不再贅述了.

在逆向時, 常用的主要有: hook eval 、hook Function 、hook JSON.stringify、JSON.parse 、hook cookie、hook window物件

四. JS和HTML互動(選修)

在HTML中,可以直接在標籤上給出一些事件的觸發

例如:頁面上的一個按鈕

<input type="button" value="點我就愛你"/>

我們能夠知道,該標籤在頁面中會產生一個按鈕,但是該按鈕無論如何進行點選. 都不會觸發任何事件

但此時, 人家其實觸發了. 只是你沒處理而已. 在點選該按鈕的時候. 瀏覽器其實收集到了點選事件.

但是由於我們沒有給出任何 發生了點選事件應該做什麼 的事情. 所以也就沒有了反應.

可以透過onclick屬性. 來給點選事件新增上具體要做什麼

<input type='button' value="點我就愛你" onclick="fn()" />


當發生點選事件時去執行fn(). fn() 是什麼? fn就是我們javascript的一個函式. 

完整程式碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script>
        function fn(){
            alert("臭不要臉")
        }
    </script>
</head>
<body>
    <input type="button" value="點我就愛你" onclick="fn()">
</body>
</html>

至此, 成功實現了 從HTML中呼叫JS

那麼在HTML中有多少種事件可以觸發呢? 非常多....記住幾個就好了

// html中的事件
click		點選事件
focus		獲取焦點
blur		失去焦點
submit		提交表單
change		更換選項
scroll		捲軸滾動
mouseover	滑鼠滑過
mouseout	滑鼠滑出
mousemove	滑鼠滑動

上述是第一種繫結事件的方案. 可以直接在html標籤中,使用onxxx系列屬性來完成事件的繫結

同時js,還提供了以下事件繫結方案:

<input type="button" id="btn" value="別點我了">
    
<script>
    // 注意:必須等到頁面載入完畢了. 才可以這樣
    document.querySelector("#btn").addEventListener("click", function(){
        console.log("你點我幹什麼?? ")
    })
</script>

document.querySelector() 給出一個css選擇器, 就可以得到一個html頁面上標籤元素的控制代碼(控制該標籤).

獲取控制代碼的方案有好多. 常見的有:

document.getElementById();         // 根據id的值 獲取控制代碼
document.getElementsByClassName(); // 根據class的值 獲取控制代碼

// <form name='myform'><input type="myusername"/></form>
document.form的name.表單元素的name;  // document.myform.myusername;

現在相當於,可以從html轉到JS中了,並且在js中可以捕獲到html中的內容了

此時 對應的表單驗證,也可以完成了

<form action="伺服器地址" id="login_form">
    <label for="username">使用者名稱:</label><input type="text" name="username" id="username"><span id="username_info"></span><br/>
    <label for="password">密碼:</label><input type="text" name="password" id="password"><span id="password_info"></span><br/>
    <input type="button" id="btn" value="點我登入">
</form>
<script>
    // 在頁面載入的時候
    window.onload = function(){
        
        // let btnEle = document.getElementById('btn')
        // btnEle.onclick = function(){
        // 等價於上面
        document.getElementById('btn').addEventListener("click", function(){
            // 清空提示資訊
            document.getElementById('username_info').innerText = ""; 
            document.getElementById('password_info').innerText = "";

            let username = document.getElementById('username').value;  // 獲取username標籤中的value屬性
            let password = document.getElementById('password').value;  // 獲取密碼
            let flag = true;  // 最終是否可以提交表單?
            if(!username){
                document.getElementById('username_info').innerText = "使用者名稱不能為空";
                flag = false;
            }

            if(!password){
                document.getElementById('password_info').innerText = "密碼不能為空";
                flag = false;
            }

            if (flag){
                document.getElementById('login_form').submit();
            }
        })
    }
</script>