我們在這篇文章中提過,隨著前端技術的演進,很多專案有把jQuery替換成Vanilla javascript的趨勢,因為最近用ChatGPT寫程式很紅,所以我也想要試試把我的幾個open source專案,也根據ChatGPT給的程式碼來把jQuery替換掉。我們來看看ChatGPT是否可以把jQuery做替換,以及會遭遇到哪問題。
替換成Vanilla javascript的對話
首先我直接把整段程式碼貼上,結果顯示太長。所以程式碼只能一小段一小段貼上,這時候我們就必須要先知道哪裡有jQuery的程式碼再貼過來:
我們來看一些轉換的對話:
把$(“#w2v_epoch”)轉成document.querySelector(“#w2v_epoch”)沒有問題。然後把text()轉成.textContent也沒有問題。看起來這個轉換還滿成功的。
再來看看其他的部分:
首先,它看到我們原本的程式$(“#nn_errors”)寫了3次。於是把$(“#nn_errors”)替換成了一個變數w2vVis來儲存,而且變數的命名也還滿微妙的。
但這時候就出現Bug了。我們可以看到它每次對width()的替換都不一樣。有時候是width,有時候是offsetWidth,有時候又變成clientWidth。
而且width這個attribute是不存在的,所以w2vVis.width和w2vVis.height的值會得到undefined,用瀏覽器跑起來雖然不會有Error Message但是該element就顯示不出來了(因為長寬都是undefined),讓我花了一個晚上找Bug和查閱jQuery文件。
在改寫width()時為何出錯?
在翻閱文件時發現,該如何把jQuery的width()用Vanilla javascript重寫時,網路上不同網站找到的答案確實不一致。而clientWidth和offsetWidth的差異如下圖,也不容易讓人理解:
歸根結底,jQuery的內部如何實作width()其實挺複雜的,有針對不同瀏覽器做處理,也有對box model做一些調整,jQuery 3.4.1版再處理width()的原始碼如下:
jQuery.each( [ "height", "width" ], function( i, dimension ) {
jQuery.cssHooks[ dimension ] = {
get: function( elem, computed, extra ) {
if ( computed ) {
// Certain elements can have dimension info if we invisibly show them
// but it must have a current display style that would benefit
return rdisplayswap.test( jQuery.css( elem, "display" ) ) &&
// Support: Safari 8+
// Table columns in Safari have non-zero offsetWidth & zero
// getBoundingClientRect().width unless display is changed.
// Support: IE <=11 only
// Running getBoundingClientRect on a disconnected node
// in IE throws an error.
( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?
swap( elem, cssShow, function() {
return getWidthOrHeight( elem, dimension, extra );
} ) :
getWidthOrHeight( elem, dimension, extra );
}
},
set: function( elem, value, extra ) {
var matches,
styles = getStyles( elem ),
// Only read styles.position if the test has a chance to fail
// to avoid forcing a reflow.
scrollboxSizeBuggy = !support.scrollboxSize() &&
styles.position === "absolute",
// To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991)
boxSizingNeeded = scrollboxSizeBuggy || extra,
isBorderBox = boxSizingNeeded &&
jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
subtract = extra ?
boxModelAdjustment(
elem,
dimension,
extra,
isBorderBox,
styles
) :
0;
// Account for unreliable border-box dimensions by comparing offset* to computed and
// faking a content-box to get border and padding (gh-3699)
if ( isBorderBox && scrollboxSizeBuggy ) {
subtract -= Math.ceil(
elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
parseFloat( styles[ dimension ] ) -
boxModelAdjustment( elem, dimension, "border", false, styles ) -
0.5
);
}
// Convert to pixels if value adjustment is needed
if ( subtract && ( matches = rcssNum.exec( value ) ) &&
( matches[ 3 ] || "px" ) !== "px" ) {
elem.style[ dimension ] = value;
value = jQuery.css( elem, dimension );
}
return setPositiveNumber( elem, value, subtract );
}
};
} );
function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) {
var i = dimension === "width" ? 1 : 0,
extra = 0,
delta = 0;
// Adjustment may not be necessary
if ( box === ( isBorderBox ? "border" : "content" ) ) {
return 0;
}
for ( ; i < 4; i += 2 ) {
// Both box models exclude margin
if ( box === "margin" ) {
delta += jQuery.css( elem, box + cssExpand[ i ], true, styles );
}
// If we get here with a content-box, we're seeking "padding" or "border" or "margin"
if ( !isBorderBox ) {
// Add padding
delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
// For "border" or "margin", add border
if ( box !== "padding" ) {
delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
// But still keep track of it otherwise
} else {
extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
}
// If we get here with a border-box (content + padding + border), we're seeking "content" or
// "padding" or "margin"
} else {
// For "content", subtract padding
if ( box === "content" ) {
delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
}
// For "content" or "padding", subtract border
if ( box !== "margin" ) {
delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
}
}
}
// Account for positive content-box scroll gutter when requested by providing computedVal
if ( !isBorderBox && computedVal >= 0 ) {
// offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border
// Assuming integer scroll gutter, subtract the rest and round down
delta += Math.max( 0, Math.ceil(
elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
computedVal -
delta -
extra -
0.5
// If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter
// Use an explicit zero to avoid NaN (gh-3964)
) ) || 0;
}
return delta;
}
function getWidthOrHeight( elem, dimension, extra ) {
// Start with computed style
var styles = getStyles( elem ),
// To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322).
// Fake content-box until we know it's needed to know the true value.
boxSizingNeeded = !support.boxSizingReliable() || extra,
isBorderBox = boxSizingNeeded &&
jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
valueIsBorderBox = isBorderBox,
val = curCSS( elem, dimension, styles ),
offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 );
// Support: Firefox <=54
// Return a confounding non-pixel value or feign ignorance, as appropriate.
if ( rnumnonpx.test( val ) ) {
if ( !extra ) {
return val;
}
val = "auto";
}
// Fall back to offsetWidth/offsetHeight when value is "auto"
// This happens for inline elements with no explicit setting (gh-3571)
// Support: Android <=4.1 - 4.3 only
// Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602)
// Support: IE 9-11 only
// Also use offsetWidth/offsetHeight for when box sizing is unreliable
// We use getClientRects() to check for hidden/disconnected.
// In those cases, the computed value can be trusted to be border-box
if ( ( !support.boxSizingReliable() && isBorderBox ||
val === "auto" ||
!parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) &&
elem.getClientRects().length ) {
isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
// Where available, offsetWidth/offsetHeight approximate border box dimensions.
// Where not available (e.g., SVG), assume unreliable box-sizing and interpret the
// retrieved value as a content box dimension.
valueIsBorderBox = offsetProp in elem;
if ( valueIsBorderBox ) {
val = elem[ offsetProp ];
}
}
// Normalize "" and auto
val = parseFloat( val ) || 0;
// Adjust for the element's box model
return ( val +
boxModelAdjustment(
elem,
dimension,
extra || ( isBorderBox ? "border" : "content" ),
valueIsBorderBox,
styles,
// Provide the current computed size to request scroll gutter calculation (gh-3589)
val
)
) + "px";
}
所以這底層的邏輯挺複雜的。
最後,我們再來試試HTML的部分,如果我們把以下HTML程式碼交給ChatGPT來移除jQuery:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Word2Vec Demo</title>
<!-- <meta charset="utf-8"> -->
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/js/bootstrap.min.js"></script>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js"></script>
<style>
b {
background-color: #84DCC6;
}
</style>
</head>
<body>
...
</body>
</html>
它確實會把jQuery的script tag移除,但它卻不知道Bootstrap 3.4.0這個版本需要jQuery;如果我們要移除jQuery的話,就必須要把Bootstrap 3升級到沒有依賴jQuery的版本(比方說Bootstrap 5)。否則就會出現以下的Error Messages:
結論
ChatGPT確實可以給出替換jQuery的程式碼,雖然可以正確地轉換一些簡單的案例(e.g., 把$(“#w2v_epoch”)轉成document.querySelector(“#w2v_epoch”)),但還是有些Bugs。儘管如此我仍然認為可以改變很多產業的生態。畢竟目前的ChatGPT只是一個通用(generic)的版本;如果我們可以專門針對特定程式語言來訓練一個新版本,肯定可以有個好的程式碼。