העברת סקריפטים לסביבת זמן ריצה של 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() הפונקציה מחזירה שנים ב-2 ספרות לשנים 1900-1999, אבל לשנים אחרות היא מחזירה ארבע ספרות ש הוא היה ההתנהגות ב-JavaScript בגרסה 1.2 ובגרסאות קודמות.

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

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

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

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.
      

דוגמאות הקוד הבאות מראות כיצד ניתן לבנות איטרטור זמן ריצה של קרנף, ואיך לבנות איטרטור חלופי בסביבת זמן ריצה של 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);
}
      

הימנעות מסעיפים מותנים של קליטה

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

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

// 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()

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

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

הבדלים אחרים

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

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

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

Date toLocaleString(), toLocaleDateString(), וtoLocaleTimeString() מתנהגים בזמן הריצה של V8 בהשוואה ל-Rhino.

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

בסביבת זמן הריצה של V8, פורמט ברירת המחדל הוא הפורמט הקצר והפרמטרים מועברים בהתאם לתקן 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 בתור פרמטרים של constructor או מאפייני אובייקט.

כשמעבירים סקריפט ל-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, באמצעות ה-JavaScript JSON.stringify() באובייקט 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 ל-method כפרמטר הסתיימה להעברת המחרוזת "undefined" ל-method הזה.

ב-V8, העברת undefined ל-methods מקבילה להעברת 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, צריך לספק למשתמשים לפחות לראות גישה לסקריפט כדי שהטריגרים של הסקריפט יפעלו כמו שצריך.