העברת סקריפטים לסביבת זמן ריצה של 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);
}
      

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

סביבת זמן הריצה של 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 הכילה method 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, האובייקט הסטנדרטי של JavaScript Error לא תומך ב-fileName או lineNumber כפרמטרים של constructor או כמאפייני אובייקט.

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

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

// 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 ל-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, פרויקט והספריות שלו פועלים בהקשרים שונים של הפעלה, ולכן יש להם רשתות שונות של רשתות '{0/}' ו'שרשראות אבות טיפוס'.

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