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

כשמעבירים את הסקריפט ל-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 נוספה תכונה שמאפשרת להוסיף איטרטור מותאם אישית לכל CLA על ידי הצהרה על הפונקציה __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 מותנים של catch..if כי הם לא תואמים לתקנים.

כשמעבירים את הסקריפט ל-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()

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

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

ב-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 הגלובלי" בקוד למעשה מעריכים את ההקשר המיוחד, שמכיל רק את הקוד והמשתנים שמוגדרים בסקריפט. השימוש ב-this לא כולל שירותי Apps Script ואובייקטים של ECMAScript מובנים. המצב הזה היה דומה למבנה 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, הפרויקט והספריות שלו פועלים בהקשרים שונים של הפעלה, ולכן כוללים רשתות שונות של globals ואב-טיפוס.

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

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

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