העברת סקריפטים לסביבת זמן ריצה של V8

אם יש לכם סקריפט קיים שמשתמש בסביבת זמן הריצה של Rhino ואתם רוצים להשתמש בתחביר ובתכונות של V8, עליכם להעביר את הסקריפט ל-V8.

רוב הסקריפטים שנכתבו באמצעות סביבת זמן הריצה של Rhino יכולים לפעול בסביבת זמן ריצה של V8 ללא התאמה. לרוב, הדרישה היחידה להוספת תחביר ותכונות של V8 לסקריפט היא הפעלת סביבת זמן הריצה של V8.

עם זאת, יש קבוצה קטנה של אי-תאימות והבדלים אחרים שיכולים לגרום לכך שסקריפט נכשל או מתנהג באופן בלתי צפוי אחרי הפעלת סביבת זמן הריצה V8. כשעוברים להשתמש ב-V8 בסקריפט, צריך לחפש את הבעיות האלה בפרויקט הסקריפט ולתקן אותן.

תהליך ההעברה ל-V8

כדי להעביר סקריפט ל-V8, פועלים לפי השלבים הבאים:

  1. מפעילים את סביבת זמן הריצה V8 עבור הסקריפט.
  2. חשוב לקרוא בעיון את האי-תאימות שמפורטות בהמשך. בודקים את הסקריפט כדי לקבוע אם יש בו אי-תאימות. אם יש אי-תאימות אחת או יותר, משנים את קוד הסקריפט כדי להסיר את הבעיה או למנוע אותה.
  3. חשוב לקרוא בעיון את הבדלים האחרים שמפורטים בהמשך. בודקים את הסקריפט כדי לקבוע אם אחד מההבדלים המפורטים משפיע על התנהגות הקוד. משנים את הסקריפט כדי לתקן את ההתנהגות.
  4. אחרי שתתקנו את כל חוסר התאימות או ההבדלים האחרים שגיליתם, תוכלו להתחיל לעדכן את הקוד כך שישתמש בתחביר V8 ובתכונות אחרות לפי הצורך.
  5. אחרי שתסיימו לשנות את הקוד, כדאי לבדוק היטב את הסקריפט כדי לוודא שהוא פועל כצפוי.
  6. אם הסקריפט הוא אפליקציית אינטרנט או תוסף שפורסם, צריך ליצור גרסה חדשה של הסקריפט עם ההתאמות ל-V8. כדי שגרסת V8 תהיה זמינה למשתמשים, צריך לפרסם מחדש את הסקריפט עם הגרסה הזו.

חוסר תאימות

לצערנו, סביבת זמן הריצה המקורית של Apps Script שמבוססת על Rhino איפשרה כמה התנהגויות לא סטנדרטיות של ECMAScript. בגלל ש-V8 תואם לתקנים, ההתנהגויות האלה לא נתמכות אחרי ההעברה. אם לא תתקנו את הבעיות האלה, הן יובילו לשגיאות או לפעולה שגויה של הסקריפט אחרי שתפעילו את סביבת זמן הריצה של V8.

בקטעים הבאים מתוארים כל אחד מההתנהגויות האלה והשלבים שצריך לבצע כדי לתקן את קוד הסקריפט במהלך ההעברה ל-V8.

הימנעו מ-for each(variable in object)

ההצהרה for each (variable in object) נוספה ל-JavaScript 1.6 והוסרה לטובת for...of.

כשמעבירים את הסקריפט ל-V8, כדאי להימנע משימוש בfor each (variable in object) הצהרות.

במקום זאת, צריך להשתמש ב-for (variable in object):

// Rhino runtime
var obj = {a: 1, b: 2, c: 3};

// Don't use 'for each' in V8
for each (var value in obj) {
  Logger.log("value = %s", value);
}
      
// V8 runtime
var obj = {a: 1, b: 2, c: 3};

for (var key in obj) {  // OK in V8
  var value = obj[key];
  Logger.log("value = %s", value);
}
      

הימנעו מ-Date.prototype.getYear()

בסביבת זמן הריצה המקורית של Rhino, הפונקציה Date.prototype.getYear() מחזירה שנים בן שתי ספרות לשנים 1900 עד 1999, אבל שנים בן ארבע ספרות לתאריכים אחרים. זו הייתה ההתנהגות ב-JavaScript 1.2 ובגרסאות קודמות.

בסביבת זמן הריצה של V8, הפונקציה Date.prototype.getYear() מחזירה במקום זאת את השנה פחות 1900, כפי שנדרש לפי תקני ECMAScript.

כשמעבירים את הסקריפט ל-V8, תמיד צריך להשתמש ב-Date.prototype.getFullYear(), שמחזיר שנה בת ארבע ספרות ללא קשר לתאריך.

הימנעות משימוש במילות מפתח שמורות כשמות

ב-ECMAScript אסור להשתמש במילות מפתח שמורות מסוימות בשמות של פונקציות ומשתנים. סביבת זמן הריצה של Rhino אפשרה להשתמש בהרבה מהמילים האלה, ולכן אם הקוד שלכם משתמש בהן, תצטרכו לשנות את השמות של הפונקציות או המשתנים.

כשמעבירים את הסקריפט ל-V8, כדאי להימנע מהענקת שמות למשתנים או לפונקציות באמצעות אחת ממילות המפתח השמורות. משנים את השם של כל משתנה או פונקציה כדי להימנע משימוש בשם של מילת המפתח. דוגמאות לשימוש נפוץ במילות מפתח בתור שמות הן class,‏ import ו-export.

הימנעות מקביעת מחדש של משתני const

בסביבת זמן הריצה המקורית של Rhino, אפשר להצהיר על משתנה באמצעות const, כלומר הערך של הסמל לא משתנה אף פעם והקצאות עתידיות לסמל מתעלמות ממנו.

בסביבת זמן הריצה החדשה של V8, מילת המפתח const תואמת לתקן, והקצאה למשתנה שמוצהר כ-const תגרום לשגיאת זמן ריצה מסוג TypeError: Assignment to constant variable.

כשעוברים את הסקריפט ל-V8, אל תנסה להקצות מחדש את הערך של משתנה const:

// Rhino runtime
const x = 1;
x = 2;          // No error
console.log(x); // Outputs 1
      
// V8 runtime
const x = 1;
x = 2;          // Throws TypeError
console.log(x); // Never executed
      

הימנעו משימוש בביטויים מילוליים של XML ובאובייקט XML

התוסף הלא סטנדרטי הזה ל-ECMAScript מאפשר לפרויקטים של Apps Script להשתמש בתחביר XML ישירות.

כשעוברים את הסקריפט ל-V8, כדאי להימנע משימוש בביטויים לינאריים ישירים של XML או באובייקט XML.

במקום זאת, משתמשים ב-XmlService כדי לנתח XML:

// V8 runtime
var incompatibleXml1 = <container><item/></container>;             // Don't use
var incompatibleXml2 = new XML('<container><item/></container>');  // Don't use

var xml3 = XmlService.parse('<container><item/></container>');     // OK
      

לא ליצור פונקציות של מערכי איטרטור בהתאמה אישית באמצעות __iterator__

ב-JavaScript 1.7 נוספה תכונה שמאפשרת להוסיף איטרטור בהתאמה אישית לכל כיתה על ידי הצהרה על פונקציית __iterator__ באב טיפוס של הכיתה. התכונה הזו נוספה גם לסביבת זמן הריצה של Rhino ב-Apps Script כדי להקל על המפתחים. עם זאת, התכונה הזו אף פעם לא הייתה חלק מתקן ECMA-262 והיא הוסרה במנועי JavaScript שתואמים ל-ECMAScript. לא ניתן להשתמש ביצירת האיטרטור הזו בסקריפטים שמשתמשים ב-V8.

כשמעבירים את הסקריפט ל-V8, כדאי להימנע משימוש בפונקציה __iterator__ כדי ליצור מערכי איטרציה מותאמים אישית. במקום זאת, צריך להשתמש במחזורים של ECMAScript 6.

נבחן את יצירת המערך הבאה:

// Create a sample array
var myArray = ['a', 'b', 'c'];
// Add a property to the array
myArray.foo = 'bar';

// The default behavior for an array is to return keys of all properties,
//  including 'foo'.
Logger.log("Normal for...in loop:");
for (var item in myArray) {
  Logger.log(item);            // Logs 0, 1, 2, foo
}

// To only log the array values with `for..in`, a custom iterator can be used.
      

בדוגמאות הקוד הבאות מוסבר איך אפשר ליצור מעבד חזרה בסביבת זמן הריצה של Rhino, ואיך ליצור מעבד חזרה חלופי בסביבת זמן הריצה של V8:

// Rhino runtime custom iterator
function ArrayIterator(array) {
  this.array = array;
  this.currentIndex = 0;
}

ArrayIterator.prototype.next = function() {
  if (this.currentIndex
      >= this.array.length) {
    throw StopIteration;
  }
  return "[" + this.currentIndex
    + "]=" + this.array[this.currentIndex++];
};

// Direct myArray to use the custom iterator
myArray.__iterator__ = function() {
  return new ArrayIterator(this);
}


Logger.log("With custom Rhino iterator:");
for (var item in myArray) {
  // Logs [0]=a, [1]=b, [2]=c
  Logger.log(item);
}
      
// V8 runtime (ECMAScript 6) custom iterator
myArray[Symbol.iterator] = function() {
  var currentIndex = 0;
  var array = this;

  return {
    next: function() {
      if (currentIndex < array.length) {
        return {
          value: "[${currentIndex}]="
            + array[currentIndex++],
          done: false};
      } else {
        return {done: true};
      }
    }
  };
}

Logger.log("With V8 custom iterator:");
// Must use for...of since
//   for...in doesn't expect an iterable.
for (var item of myArray) {
  // Logs [0]=a, [1]=b, [2]=c
  Logger.log(item);
}
      

הימנעות משימוש בתנאי ליצירת קלעיות catch

סביבת זמן הריצה של V8 לא תומכת בתנאי catch..if לתנאי תפיסה (catch), כי הם לא תואמים לתקן.

כשמעבירים את הסקריפט ל-V8, צריך להעביר את כל התנאים של ה-catch לגוף ה-catch:

// Rhino runtime

try {
  doSomething();
} catch (e if e instanceof TypeError) {  // Don't use
  // Handle exception
}
      
// V8 runtime
try {
  doSomething();
} catch (e) {
  if (e instanceof TypeError) {
    // Handle exception
  }
}

להימנע משימוש ב-Object.prototype.toSource()

גרסה 1.3 של JavaScript הכילה את השיטה Object.prototype.toSource(), שמעולם לא הייתה חלק מתקן ECMAScript. הוא לא נתמך בסביבת זמן הריצה של V8.

כשמעבירים את הסקריפט ל-V8, צריך להסיר מהקוד כל שימוש ב-Object.prototype.toSource().

הבדלים אחרים

בנוסף לאי-התאמות שלמעלה שעלולות לגרום לכשלים בסקריפטים, יש כמה הבדלים נוספים שיכולים לגרום להתנהגות בלתי צפויה של סקריפטים בסביבת זמן הריצה של V8, אם לא מתקנים אותם.

בקטעים הבאים מוסבר איך לעדכן את קוד הסקריפט כדי להימנע מהפתעות לא צפויות כאלה.

שינוי הפורמט של התאריך והשעה בהתאם לאזור

השיטות toLocaleString(),‏ toLocaleDateString() ו-toLocaleTimeString() ב-Date פועלות באופן שונה בסביבת זמן הריצה של V8 בהשוואה ל-Rhino.

ב-Rhino, פורמט ברירת המחדל הוא הפורמט הארוך, וכל הפרמטרים המועברים מתעלמים.

בסביבת זמן הריצה של V8, פורמט ברירת המחדל הוא short format והטיפול בפרמטרים המועברים מתבצע בהתאם לתקן ECMA (פרטים נוספים זמינים במסמכי העזרה של toLocaleDateString()).

כשעוברים את הסקריפט ל-V8, צריך לבדוק ולשנות את הציפיות של הקוד לגבי הפלט של שיטות תאריך ושעה ספציפיות לאזור:

// Rhino runtime
var event = new Date(
  Date.UTC(2012, 11, 21, 12));

// Outputs "December 21, 2012" in Rhino
console.log(event.toLocaleDateString());

// Also outputs "December 21, 2012",
//  ignoring the parameters passed in.
console.log(event.toLocaleDateString(
    'de-DE',
    { year: 'numeric',
      month: 'long',
      day: 'numeric' }));
// V8 runtime
var event = new Date(
  Date.UTC(2012, 11, 21, 12));

// Outputs "12/21/2012" in V8
console.log(event.toLocaleDateString());

// Outputs "21. Dezember 2012"
console.log(event.toLocaleDateString(
    'de-DE',
    { year: 'numeric',
      month: 'long',
      day: 'numeric' }));
      

להימנע משימוש ב-Error.fileName וב-Error.lineNumber

ב-V8 untime, אובייקט ה-JavaScript הסטנדרטי Error לא תומך ב-fileName או ב-lineNumber כפרמטרים של קונסטרוקטור או כמאפייני אובייקט.

כשמעבירים את הסקריפט ל-V8, צריך להסיר כל תלות ב-Error.fileName וב-Error.lineNumber.

אפשרות אחרת היא להשתמש ב-Error.prototype.stack. גם הסטאק הזה לא סטנדרטי, אבל יש תמיכה בו גם ב-Rhino וגם ב-V8. הפורמט של מעקב ה-stack שנוצר על ידי שתי הפלטפורמות שונה במקצת:

// Rhino runtime Error.prototype.stack
// stack trace format
at filename:92 (innerFunction)
at filename:97 (outerFunction)
// V8 runtime Error.prototype.stack
// stack trace format
Error: error message
at innerFunction (filename:92:11)
at outerFunction (filename:97:5)
      

שינוי הטיפול באובייקטים של enum שהומרו למחרוזת

בסביבת זמן הריצה המקורית של Rhino, שימוש ב-method‏ JSON.stringify() של JavaScript באובייקט enum מחזיר רק את הערך {}.

ב-V8, שימוש באותה שיטה באובייקט enum מחזיר את שם ה-enum.

כשעוברים את הסקריפט ל-V8, צריך לבדוק את הציפיות של הקוד לגבי הפלט של JSON.stringify() באובייקטים של enum ולשנות אותן לפי הצורך:

// Rhino runtime
var enumName =
  JSON.stringify(Charts.ChartType.BUBBLE);

// enumName evaluates to {}
// V8 runtime
var enumName =
  JSON.stringify(Charts.ChartType.BUBBLE);

// enumName evaluates to "BUBBLE"

שינוי הטיפול בפרמטרים לא מוגדרים

בסביבת זמן הריצה המקורית של Rhino, העברת undefined לשיטה כפרמטר הביאה להעברת המחרוזת "undefined" לשיטה הזו.

ב-V8, העברת undefined לשיטות זהה להעברת null.

כשמעבירים את הסקריפט ל-V8, צריך לבדוק ולשנות את הציפיות של הקוד לגבי הפרמטרים של undefined:

// Rhino runtime
SpreadsheetApp.getActiveRange()
    .setValue(undefined);

// The active range now has the string
// "undefined"  as its value.
      
// V8 runtime
SpreadsheetApp.getActiveRange()
    .setValue(undefined);

// The active range now has no content, as
// setValue(null) removes content from
// ranges.

שינוי הטיפול ב-this ברמת הארגון

סביבת זמן הריצה של Rhino מגדירה הקשר מיוחד מרומז לסקריפטים שמשתמשים בו. קוד הסקריפט פועל בהקשר המשתמעים הזה, בניגוד ל-this הגלובלי בפועל. כלומר, הפניות ל-'this הגלובלי' בקוד למעשה מקבלות ערך בהתאם להקשר המיוחד, שמכיל רק את הקוד והמשתנים שהוגדרו בסקריפט. השירותים המובנים של Apps Script ואובייקטים של ECMAScript לא נכללים בשימוש הזה ב-this. המצב הזה היה דומה למבנה ה-JavaScript הזה:

// Rhino runtime

// Apps Script built-in services defined here, in the actual global context.
var SpreadsheetApp = {
  openById: function() { ... }
  getActive: function() { ... }
  // etc.
};

function() {
  // Implicit special context; all your code goes here. If the global this
  // is referenced in your code, it only contains elements from this context.

  // Any global variables you defined.
  var x = 42;

  // Your script functions.
  function myFunction() {
    ...
  }
  // End of your code.
}();

בגרסה V8, ההקשר המיוחד המשתמעים הוסר. משתנים ופונקציות גלובליים שמוגדרים בסקריפט ממוקמים בהקשר הגלובלי, לצד שירותי Apps Script מובנים ורכיבים מובנים של ECMAScript כמו Math ו-Date.

כשעוברים את הסקריפט ל-V8, צריך לבדוק ולשנות את הציפיות של הקוד לגבי השימוש ב-this בהקשר גלובלי. ברוב המקרים, ההבדלים גלויים רק אם הקוד שלכם בודק את המפתחות או שמות המאפיינים של האובייקט הגלובלי this:

// Rhino runtime
var myGlobal = 5;

function myFunction() {

  // Only logs [myFunction, myGlobal];
  console.log(Object.keys(this));

  // Only logs [myFunction, myGlobal];
  console.log(
    Object.getOwnPropertyNames(this));
}





      
// V8 runtime
var myGlobal = 5;

function myFunction() {

  // Logs an array that includes the names
  // of Apps Script services
  // (CalendarApp, GmailApp, etc.) in
  // addition to myFunction and myGlobal.
  console.log(Object.keys(this));

  // Logs an array that includes the same
  // values as above, and also includes
  // ECMAScript built-ins like Math, Date,
  // and Object.
  console.log(
    Object.getOwnPropertyNames(this));
}

שינוי הטיפול ב-instanceof בספריות

שימוש ב-instanceof בספרייה על אובייקט שמוענק כפרמטר בפונקציה מפרויקט אחר עלול להוביל לתוצאות שליליות שגויות. בסביבת זמן הריצה של V8, פרויקט והספריות שלו פועלים בהקשרי ביצוע שונים, ולכן יש להם משתנים גלובליים ורשתות אב טיפוס שונים.

שימו לב שזה קורה רק אם בספרייה נעשה שימוש ב-instanceof באובייקט שלא נוצר בפרויקט. השימוש בו באובייקט שנוצר בפרויקט, בין שבאותו סקריפט ובין שבסקריפט אחר בפרויקט, אמור לפעול כצפוי.

אם פרויקט שפועל ב-V8 משתמש בסקריפט שלכם כספרייה, כדאי לבדוק אם בסקריפט נעשה שימוש ב-instanceof בפרמטר שיוענק מפרויקט אחר. משנים את השימוש ב-instanceof ומשתמשים בחלופות אפשריות אחרות בהתאם למקרה השימוש.

אחת מהחלופות ל-a instanceof b היא להשתמש ב-constructor של a במקרים שבהם אין צורך לחפש את כל שרשרת האב טיפוס, אלא רק לבדוק את ה-constructor. שימוש: a.constructor.name == "b"

נניח שיש שני פרויקטים, פרויקט א' ופרויקט ב', שבהם פרויקט א' משתמש בפרויקט ב' כספרייה.

//Rhino runtime

//Project A

function caller() {
   var date = new Date();
   // Returns true
   return B.callee(date);
}

//Project B

function callee(date) {
   // Returns true
   return(date instanceof Date);
}

      
//V8 runtime

//Project A

function caller() {
   var date = new Date();
   // Returns false
   return B.callee(date);
}

//Project B

function callee(date) {
   // Incorrectly returns false
   return(date instanceof Date);
   // Consider using return (date.constructor.name ==
   // Date) instead.
   // return (date.constructor.name == Date) -> Returns
   // true
}

אפשרות אחרת היא להוסיף פונקציה שבודקת את instanceof בפרויקט הראשי ומעבירה את הפונקציה בנוסף לפרמטרים אחרים כשקוראים לפונקציה בספרייה. לאחר מכן אפשר להשתמש בפונקציה שהועברה כדי לבדוק את instanceof בתוך הספרייה.

//V8 runtime

//Project A

function caller() {
   var date = new Date();
   // Returns True
   return B.callee(date, date => date instanceof Date);
}

//Project B

function callee(date, checkInstanceOf) {
  // Returns True
  return checkInstanceOf(date);
}
      

שינוי העברת המשאבים שאינם משותפים לספריות

העברת משאב לא משותף מהסקריפט הראשי לספרייה פועלת באופן שונה בסביבת זמן הריצה של V8.

בסביבת זמן הריצה של Rhino, העברת משאב לא משותף לא תפעל. במקום זאת, הספרייה משתמשת במשאב משלה.

בסביבת V8, העברת משאב לא משותף לספרייה פועלת. הספרייה משתמשת במשאב שאינו משותף שהוענק לה.

אין להעביר משאבים לא משותפים כפרמטרים של פונקציות. תמיד צריך להצהיר על משאבים לא משותפים באותו סקריפט שבו משתמשים בהם.

נניח שיש שני פרויקטים, פרויקט א' ופרויקט ב', שבהם פרויקט א' משתמש בפרויקט ב' כספרייה. בדוגמה הזו, PropertiesService הוא משאב לא משותף.

// Rhino runtime
// Project A
function testPassingNonSharedProperties() {
  PropertiesService.getScriptProperties()
      .setProperty('project', 'Project-A');
  B.setScriptProperties();
  // Prints: Project-B
  Logger.log(B.getScriptProperties(
      PropertiesService, 'project'));
}

//Project B function setScriptProperties() { PropertiesService.getScriptProperties() .setProperty('project', 'Project-B'); } function getScriptProperties( propertiesService, key) { return propertiesService.getScriptProperties() .getProperty(key); }

// V8 runtime
// Project A
function testPassingNonSharedProperties() {
  PropertiesService.getScriptProperties()
      .setProperty('project', 'Project-A');
  B.setScriptProperties();
  // Prints: Project-A
  Logger.log(B.getScriptProperties(
      PropertiesService, 'project'));
}

// Project B function setProperties() { PropertiesService.getScriptProperties() .setProperty('project', 'Project-B'); } function getScriptProperties( propertiesService, key) { return propertiesService.getScriptProperties() .getProperty(key); }

עדכון הגישה לסקריפטים עצמאיים

כדי שהטריגרים של סקריפטים עצמאיים שפועלים בסביבת זמן הריצה V8 יפעלו כמו שצריך, צריך לתת למשתמשים לפחות הרשאת צפייה בסקריפט.