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

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

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

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

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

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

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

חוסר תאימות

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

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