有沒有這種經驗:同事在 E 欄改了「完成」,你想讓腳本自動把 F 欄填上完成時間、整列變淡綠,還要順便檢查數量是否合法。
其實核心就一句:先知道「哪一列被改到」。getRow() 回傳當前選區左上角的列號(1-based),讓你把所有動作鎖在正確那一列上。
本文從實務角度下手:帶你用 onEdit(e.range.getRow()) 做同列回寫、如何搭配 getColumn()、getNumRows() 算出範圍終點、以及大量操作時的效能做法(一次讀寫、由下而上刪列)。
也會把常見雷點攤開來說:把 Sheet 當 Range 用、把 0-based 陣列當 1-based 列、以為 getRow() 會理可見列順序…通通逐一拆解。看完,你就能把看似零碎的流程,串成穩定又好維護的自動化。希望本篇文章能夠幫助到需要的您。
目錄
{tocify} $title={目錄}
為什麼是 getRow()?核心概念先釐清
所屬類別:Range(不是 Sheet、不是 Spreadsheet)。
回傳型別:Number(整數)。
起算方式:1 為第一列(Apps Script 的試算表列、欄皆為 1-based)。
對應位置:若 Range 是多格(例如 B3:D8),getRow() 回傳 3,因為它取左上角儲存格(B3)的列。
與其它親戚:
getColumn():回傳左上角欄號。
getLastRow():整張工作表最後一列有資料的列號(屬於 Sheet)。
getMaxRows():工作表目前的總列數(屬於 Sheet)。
Advanced Sheets API 的 rowIndex 是 0-based,跟 Apps Script 的 getRow() 完全不同,別混。
一句話總結:getRow() 幫你在「我現在處理的這個範圍」與「整張表的絕對列號」之間搭橋。
最常見的四種使用場景
onEdit:依編輯列決定流程
function onEdit(e) {
if (!e) return; // 保險起見
const sh = e.range.getSheet();
if (sh.getName() !== '表單') return; // 只處理特定工作表
const row = e.range.getRow(); // 重點:拿到被編輯的「列」
const col = e.range.getColumn();
// 範例:只有當使用者修改「狀態」欄(第 5 欄)才處理
if (col === 5 && row > 1) { // 跳過標題列
const status = sh.getRange(row, 5).getValue();
if (status === '完成') {
sh.getRange(row, 6).setValue(new Date()); // 在「完成時間」欄寫入時間戳
}
}
}
略過標題列、只跑資料列
function processDataRows() {
const sh = SpreadsheetApp.getActiveSheet();
const last = sh.getLastRow();
for (let r = 2; r <= last; r++) { // 從第 2 列開始
const val = sh.getRange(r, 1).getValue();
// TODO: 對每一列做事
}
}
搭配 getRow() 的思路是:當你從某個 Range 切入(例如 getActiveRange()),要判斷「是否為資料列」,最直覺就是 range.getRow() > 1。
定位多格範圍的左上角列
function topLeftRowDemo() {
const sh = SpreadsheetApp.getActiveSheet();
const range = sh.getRange('B3:D8');
const topLeftRow = range.getRow(); // 3
Logger.log(topLeftRow);
}
搭配 find 結果或條件篩選,回寫到同列其他欄
function updateSameRowByKey(key) {
const sh = SpreadsheetApp.getActiveSheet();
const values = sh.getRange(1, 1, sh.getLastRow(), 1).getValues(); // A 欄
for (let i = 0; i < values.length; i++) {
if (values[i][0] === key) {
const row = i + 1; // 因為 getValues() 是 0-based 陣列
sh.getRange(row, 2).setValue('Hit'); // 在同列 B 欄回寫
break;
}
}
}
雖然這段沒有直接呼叫 getRow(),但概念類似:把相對索引換成絕對列號,以便同列寫入。
一步步操作:從 0 開始實作 getRow() 常見任務
情境 A:建立「狀態看板」欄位自動化
目的:使用者在「狀態」欄(E 欄)改成「完成」,同列自動填上完成時間、把整列底色改為淡綠。
步驟:
1. 開啟 Apps Script 編輯器 → 新增程式。
2. 貼上:
function onEdit(e) {
if (!e) return;
const sh = e.range.getSheet();
if (sh.getName() !== '任務清單') return;
const row = e.range.getRow();
const col = e.range.getColumn();
// 標題列不處理
if (row === 1) return;
// 只在 E 欄(狀態)動作
if (col === 5) {
const status = sh.getRange(row, 5).getValue();
if (status === '完成') {
sh.getRange(row, 6).setValue(new Date()); // 完成時間(F 欄)
sh.getRange(row, 1, 1, sh.getLastColumn())
.setBackground('#e8f5e9'); // 整列淡綠
} else {
sh.getRange(row, 6).clearContent();
sh.getRange(row, 1, 1, sh.getLastColumn())
.setBackground(null); // 還原底色
}
}
}
3. 儲存、回到試算表測試:修改 E 欄,觀察是否正確改動同一列。
關鍵點:e.range.getRow() 是判斷「哪一列被動到」的直覺解法;你無須再去搜尋座標。
情境 B:表單回應工作表,自動編號與驗證
目的:每次有新資料填入時,於同列自動帶入遞增編號、同時檢查必填欄。
步驟:
1. 表單連到試算表,假設回應寫在 回應 1 工作表。
2. 使用「安裝觸發條件」→ 選擇「試算表 → 試算表變更(onChange)」或「表單提交(onFormSubmit)」;若走 onEdit 也可,但表單寫入通常用 onFormSubmit 最穩。
3. 程式碼示意(onFormSubmit):
function onFormSubmit(e) {
const sh = e.range.getSheet(); // 仍然可以用 e.range
const row = e.range.getRow(); // 拿到此次寫入的起始列
const lastCol = sh.getLastColumn();
// 自動編號(假設 A 欄為流水號)
const idCell = sh.getRange(row, 1);
if (!idCell.getValue()) {
const prev = sh.getRange(row - 1, 1).getValue();
const nextId = (Number(prev) || 0) + 1;
idCell.setValue(nextId);
}
// 必填檢查(例如 C 欄、D 欄)
const mustC = sh.getRange(row, 3).getValue();
const mustD = sh.getRange(row, 4).getValue();
if (!mustC || !mustD) {
sh.getRange(row, 1, 1, lastCol).setBackground('#ffebee'); // 淡紅
// 也可寄信或寫入備註
sh.getRange(row, 2).setNote('缺少必填欄位,請補齊');
}
}
關鍵點:表單一次寫入可能是「整列」或多欄範圍的左上角,getRow() 讓你快速定位「此次寫入的起點列」。
情境 C:多選區編輯(合併儲存格/矩形範圍)
目的:使用者一次選了 B3:D8 來套用格式,我們只想以左上角列當基準做判斷。
function formatByTopLeft() {
const range = SpreadsheetApp.getActiveRange();
const row = range.getRow(); // 3
const col = range.getColumn(); // 2 (B)
// 例如:僅在選取的左上列做標記
const sh = range.getSheet();
sh.getRange(row, 1, 1, sh.getLastColumn()).setBackground('#fff3e0');
}
與其它方法的比較與搭配
getRow() vs getLastRow()
前者回傳「這個範圍的左上角列」。
後者回傳「整張工作表最後有資料的列」。
常見搭配:用 getRow() 知道事件發生在哪一列;用 getLastRow() 來界定資料邊界。
getRow() vs 陣列索引(getValues())
getValues() 讀出二維陣列(0-based),需要自算 rowIndex + 起始列 才能回到絕對列。
getRow() 直接給你絕對列,適合事件導向、互動式處理。
getRow() + getColumn()
一次取得左上角座標,對於要「同步寫回同列/同欄」尤其好用。
getRow() + 篩選器/隱藏列
getRow() 回的永遠是絕對列號,與是否被隱藏、是否通過篩選無關。若你需要「可見列序號」,就得額外計算。
實務範例包
避免刪除標題列的安全檢查
function safeDeleteRow(row) {
const sh = SpreadsheetApp.getActiveSheet();
if (row <= 1) throw new Error('不允許刪除標題列');
sh.deleteRow(row);
}
批次刪列(避免索引位移的寫法)
function deleteRowsByStatus() {
const sh = SpreadsheetApp.getActiveSheet();
const last = sh.getLastRow();
// 從下往上刪,避免列位移
for (let r = last; r >= 2; r--) {
const status = sh.getRange(r, 5).getValue();
if (status === '作廢') {
sh.deleteRow(r);
}
}
}
欄位驗證:只對被編輯列生效
function onEdit(e) {
if (!e) return;
const sh = e.range.getSheet();
if (sh.getName() !== '訂單') return;
const row = e.range.getRow();
if (row === 1) return; // 標題
// 規定:數量(C 欄)必須為正整數
const qty = sh.getRange(row, 3).getValue();
if (!(Number.isInteger(qty) && qty > 0)) {
sh.getRange(row, 3).setBackground('#ffebee').setNote('數量需為正整數');
} else {
sh.getRange(row, 3).setBackground(null).setNote('');
}
}
合併儲存格的注意(只會回左上角)
function mergedCellsDemo() {
const sh = SpreadsheetApp.getActiveSheet();
// 假設 B3:D3 合併
const range = sh.getRange('B3:D3');
Logger.log(range.getRow()); // 3
// 若你需要「涵蓋幾列」,請用 getNumRows()
Logger.log(range.getNumRows()); // 1
}
用 getRow() 鎖定當列資料做 API
呼叫
function onEditCallApi(e) {
if (!e) return;
const sh = e.range.getSheet();
if (sh.getName() !== '出貨') return;
const row = e.range.getRow();
const tracking = sh.getRange(row, 4).getValue(); // 物流單號
if (!tracking) return;
// 以單號呼叫外部 API(假裝)
const info = { status: 'In Transit', eta: '2025-10-10' };
sh.getRange(row, 5, 1, 2).setValues([[info.status, info.eta]]);
}
效能與維護:讓 getRow() 走得更遠
1. 減少單格 I/O:需要讀/寫多欄時,盡量以區塊方式一次 getRange(...).getValues() / setValues(),比逐格快非常多。
2. 條件先算清楚再動手:先用 getRow()、getColumn() 判斷是否在你關心的區域,再去讀寫資料,避免多餘操作。
3. 避免在 onEdit 中做重計算:像是整表 getDataRange().getValues() 這類重操作,盡量不要在每次編輯都跑。
4. 錯誤處理與備註:對於不合規的輸入,用 setNote() 說明原因;未必每次都要 throw。
5. 由下而上刪列:前面示範過,避免索引位移導致「跳刪」。
針對不同 sheet 做白名單:if (sh.getName() !== 'xxx') return; 是維護的救命繩。
getRow() 雷點清單
1. 把 getRow() 用在 Sheet 上
症狀:TypeError: getRow is not a function
因為 getRow() 是 Range 的方法。若你只有工作表,先拿範圍:sheet.getRange(r, c)。
2. 把 0-based 陣列當 1-based 列
用 getValues() 迴圈時,i 從 0 開始;要換成列號,請 i + 起始列。最常見 off-by-one 錯誤就在這。
3. 與 Advanced Sheets API 混用
Advanced API 的 rowIndex 是 0-based。Apps Script getRow() 是 1-based。跨 API 時務必轉換。
4. 合併儲存格的誤會
getRow() 永遠回左上角列。以為會回所有涵蓋列,會寫到錯地方。需要範圍高度請用 getNumRows()。
5. 篩選/隱藏列造成的期望落差
getRow() 不理會可見與否;它給的是絕對列號。如果你想「第幾個可見列」,得自己計算可見列清單。
6. 在 onEdit 中大量操作整表
用戶每打一個字就掃全表,腳本很快會被節流或超時。先過濾:if (col !== 你要的欄) return;。
7. 批次刪列時由上而下
會因列位移漏刪。記得由下而上處理。
8. 假設 e 一定存在
手動執行 onEdit、或其他入口呼叫沒有事件物件 e,直接用會報錯。先 if (!e) return;。
9. 以為工作表永遠有標題列
你的腳本可能在空表就被觸發。請對 row === 1、getLastRow() === 0 這類邊界情況做保護。
10. 把 getRow() 當成資料高度
它不是範圍高度,請用 getNumRows();要終點列,用 getRow() + getNumRows() - 1。
除錯心法與排查清單
1. 先 Logger.log(e.range.getA1Notation(), e.range.getRow(), e.range.getColumn()) 看看你以為的座標是不是事實。
2. 檢查工作表名:if (sh.getName() !== 'XXX') return; 很常救命。
3. 確認你的觸發型別:onEdit、onChange、onFormSubmit 行為不同。
4. 若牽涉到合併儲存格,先取消合併再試,驗證是否是根因。
5. 大量操作前,先把資料讀成陣列在記憶體算完,再一次 setValues()。
問題集
Q1:選了 B3:D8,getRow() 回多少?
A:回 3。永遠是左上角列。
Q2:想要知道選取範圍的最後一列?
A:const endRow = range.getRow() + range.getNumRows() - 1;
Q3:為什麼 getRow() 在我這邊報 not a function?
A:你很可能對著 Sheet 或 Spreadsheet 呼叫了它。請先拿到 Range。
Q4:用篩選器後,getRow() 會跳號嗎?
A:會,以絕對列號回傳,與可見與否無關。
Q5:我用表單提交觸發,e.range.getRow() 一定等於最後一列嗎?
A:不一定,但通常會是新寫入的起始列。實務上還是用 e.range 為準,別硬推 getLastRow()。
