diff --git a/includes/common.inc b/includes/common.inc
index 89a2c4a365..e97b9aa23e 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -4309,6 +4309,7 @@ function drupal_add_js($data = NULL, $options = NULL) {
           'group' => JS_LIBRARY,
           'every_page' => TRUE,
           'weight' => 0,
+          'attributes' => array(),
         ),
         'misc/drupal.js' => array(
           'data' => 'misc/drupal.js',
@@ -4321,6 +4322,7 @@ function drupal_add_js($data = NULL, $options = NULL) {
           'preprocess' => TRUE,
           'cache' => TRUE,
           'defer' => FALSE,
+          'attributes' => array(),
         ),
       );
       $javascript = drupal_array_merge_deep($javascript, $default_javascript);
@@ -4346,6 +4348,7 @@ function drupal_add_js($data = NULL, $options = NULL) {
         $javascript[$options['data']] = $options;
     }
   }
+
   return $javascript;
 }
 
@@ -4371,6 +4374,7 @@ function drupal_js_defaults($data = NULL) {
     'preprocess' => TRUE,
     'version' => NULL,
     'data' => $data,
+    'attributes' => array(),
   );
 }
 
@@ -4392,7 +4396,7 @@ function drupal_js_defaults($data = NULL) {
  * @param $scope
  *   (optional) The scope for which the JavaScript rules should be returned.
  *   Defaults to 'header'.
- * @param $javascript
+ * @param array $javascript
  *   (optional) An array with all JavaScript code. Defaults to the default
  *   JavaScript array for the given scope.
  * @param $skip_alter
@@ -4400,14 +4404,14 @@ function drupal_js_defaults($data = NULL) {
  *   $javascript, useful when the calling function passes a $javascript array
  *   that has already been altered.
  *
- * @return
+ * @return string
  *   All JavaScript code segments and includes for the scope as HTML tags.
  *
  * @see drupal_add_js()
  * @see locale_js_alter()
  * @see drupal_js_defaults()
  */
-function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALSE) {
+function drupal_get_js($scope = 'header', array $javascript = NULL, $skip_alter = FALSE) {
   if (!isset($javascript)) {
     $javascript = drupal_add_js();
   }
@@ -4426,19 +4430,55 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
   }
 
   // Filter out elements of the given scope.
-  $items = array();
-  foreach ($javascript as $key => $item) {
-    if ($item['scope'] == $scope) {
-      $items[$key] = $item;
+  $items = array_filter(
+    $javascript,
+    function ($item) use ($scope) {
+      return $item['scope'] === $scope;
     }
+  );
+
+  // Sort the JavaScript so that it appears in the correct order.
+  uasort($items, 'drupal_sort_css_js');
+
+  // Provide the page with information about the individual JavaScript files
+  // used, information not otherwise available when aggregation is enabled.
+  $setting['ajaxPageState']['js'] = array_fill_keys(array_keys($items), 1);
+  unset($setting['ajaxPageState']['js']['settings']);
+  drupal_add_js($setting, 'setting');
+
+  // If we're outputting the header scope, then this might be the final time
+  // that drupal_get_js() is running, so add the setting to this output as well
+  // as to the drupal_add_js() cache. If $items['settings'] doesn't exist, it's
+  // because drupal_get_js() was intentionally passed a $javascript argument
+  // stripped off settings, potentially in order to override how settings get
+  // output, so in this case, do not add the setting to this output.
+  if ($scope == 'header' && isset($items['settings'])) {
+    $items['settings']['data'][] = $setting;
   }
 
-  $output = '';
-  // The index counter is used to keep aggregated and non-aggregated files in
-  // order by weight.
-  $index = 1;
-  $processed = array();
-  $files = array();
+  $elements = array(
+    '#type' => 'scripts',
+    '#items' => $items,
+  );
+
+  return drupal_render($elements);
+}
+
+/**
+ * #pre_render callback to add the elements needed for scripts tags to be
+ * rendered.
+ *
+ * @param $elements
+ *   A render array containing:
+ *   - '#items': The JS items as returned by drupal_add_js() and altered by
+ *     drupal_get_js().
+ *
+ * @return array
+ *   A render array that will render to a string of XHTML "<script>" tags.
+ *
+ * @see drupal_get_js()
+ */
+function drupal_pre_render_scripts(array $elements) {
   $preprocess_js = (variable_get('preprocess_js', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update'));
 
   // A dummy query-string is added to filenames, to gain control over
@@ -4459,65 +4499,62 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
   // third-party code might require the use of a different query string.
   $js_version_string = variable_get('drupal_js_version_query_string', 'v=');
 
-  // Sort the JavaScript so that it appears in the correct order.
-  uasort($items, 'drupal_sort_css_js');
-
-  // Provide the page with information about the individual JavaScript files
-  // used, information not otherwise available when aggregation is enabled.
-  $setting['ajaxPageState']['js'] = array_fill_keys(array_keys($items), 1);
-  unset($setting['ajaxPageState']['js']['settings']);
-  drupal_add_js($setting, 'setting');
-
-  // If we're outputting the header scope, then this might be the final time
-  // that drupal_get_js() is running, so add the setting to this output as well
-  // as to the drupal_add_js() cache. If $items['settings'] doesn't exist, it's
-  // because drupal_get_js() was intentionally passed a $javascript argument
-  // stripped off settings, potentially in order to override how settings get
-  // output, so in this case, do not add the setting to this output.
-  if ($scope == 'header' && isset($items['settings'])) {
-    $items['settings']['data'][] = $setting;
-  }
-
   // Loop through the JavaScript to construct the rendered output.
   $element = array(
+    '#type' => 'html_tag',
     '#tag' => 'script',
     '#value' => '',
     '#attributes' => array(
       'type' => 'text/javascript',
     ),
   );
-  foreach ($items as $item) {
-    $query_string =  empty($item['version']) ? $default_query_string : $js_version_string . $item['version'];
+
+  $index = 1;
+  $items = $settings = $files = array();
+
+  foreach ($elements['#items'] as $item) {
+    $js_element = $element;
+    $js_element['#attributes'] = array();
+    $js_element['#attributes'] += $item['attributes'];
+    $js_element['#attributes'] += array(
+      'type' => 'text/javascript',
+    );
+
+    $query_string = empty($item['#version']) ?
+      $default_query_string :
+      $js_version_string . $item['#version'];
 
     switch ($item['type']) {
       case 'setting':
-        $js_element = $element;
         $js_element['#value_prefix'] = $embed_prefix;
-        $js_element['#value'] = 'jQuery.extend(Drupal.settings, ' . drupal_json_encode(drupal_array_merge_deep_array($item['data'])) . ");";
+        $js_element['#value'] = 'jQuery.extend(Drupal.settings, ' .
+          drupal_json_encode(
+            drupal_array_merge_deep_array(
+              $item['data']
+            )
+          ) . ");";
         $js_element['#value_suffix'] = $embed_suffix;
-        $output .= theme('html_tag', array('element' => $js_element));
+        $settings[$index++] = $js_element;
         break;
 
       case 'inline':
-        $js_element = $element;
         if ($item['defer']) {
           $js_element['#attributes']['defer'] = 'defer';
         }
         $js_element['#value_prefix'] = $embed_prefix;
         $js_element['#value'] = $item['data'];
         $js_element['#value_suffix'] = $embed_suffix;
-        $processed[$index++] = theme('html_tag', array('element' => $js_element));
+        $items[$index++] = $js_element;
         break;
 
       case 'file':
-        $js_element = $element;
         if (!$item['preprocess'] || !$preprocess_js) {
           if ($item['defer']) {
             $js_element['#attributes']['defer'] = 'defer';
           }
           $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?';
           $js_element['#attributes']['src'] = file_create_url($item['data']) . $query_string_separator . ($item['cache'] ? $query_string : REQUEST_TIME);
-          $processed[$index++] = theme('html_tag', array('element' => $js_element));
+          $items[$index++] = $js_element;
         }
         else {
           // By increasing the index for each aggregated file, we maintain
@@ -4528,19 +4565,18 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
           // leading to better front-end performance of a website as a whole.
           // See drupal_add_js() for details.
           $key = 'aggregate_' . $item['group'] . '_' . $item['every_page'] . '_' . $index;
-          $processed[$key] = '';
           $files[$key][$item['data']] = $item;
+          $items[$index++] = '';
         }
         break;
 
       case 'external':
-        $js_element = $element;
         // Preprocessing for external JavaScript files is ignored.
         if ($item['defer']) {
           $js_element['#attributes']['defer'] = 'defer';
         }
         $js_element['#attributes']['src'] = $item['data'];
-        $processed[$index++] = theme('html_tag', array('element' => $js_element));
+        $items[$index++] = $js_element;
         break;
     }
   }
@@ -4555,14 +4591,12 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
         $preprocess_file = file_create_url($uri);
         $js_element = $element;
         $js_element['#attributes']['src'] = $preprocess_file;
-        $processed[$key] = theme('html_tag', array('element' => $js_element));
+        $items[$key] = $js_element;
       }
     }
   }
 
-  // Keep the order of JS files consistent as some are preprocessed and others are not.
-  // Make sure any inline or JS setting variables appear last after libraries have loaded.
-  return implode('', $processed) . $output;
+  return array_merge($items, $settings);
 }
 
 /**
diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test
index 0490dd53e1..1b954f944a 100644
--- a/modules/simpletest/tests/common.test
+++ b/modules/simpletest/tests/common.test
@@ -1795,6 +1795,42 @@ class JavaScriptTestCase extends DrupalWebTestCase {
     $this->assertIdentical($result, $expected, 'JavaScript is added in the expected weight order.');
   }
 
+  /**
+   * Test JavaScript with attributes rendering.
+   */
+  function testRenderAttributes() {
+    drupal_add_js(
+      'http://example.com/example.js',
+      array(
+        'type' => 'external',
+        'scope' => 'footer',
+        'weight' => -5,
+        'attributes' => array(
+          'id' => 'example',
+          'src' => 'http://com.example/example.js',
+        )
+      )
+    );
+
+    // Retrieve the rendered JavaScript and test against the regex.
+    $js = drupal_get_js('footer');
+
+    $expected = array(
+      'id="example"',
+      'src="http://example.com/example.js"',
+      'type="text/javascript"',
+    );
+
+    foreach ($expected as $expected_string) {
+      $this->assertNotEqual(
+        strpos($js, $expected_string),
+        FALSE,
+        'JavaScript with attributes has been properly rendered.'
+      );
+    }
+  }
+
+
   /**
    * Test rendering the JavaScript with a file's weight above jQuery's.
    */
diff --git a/modules/system/system.module b/modules/system/system.module
index 53844d878f..779859c814 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -323,6 +323,10 @@ function system_element_info() {
     '#group_callback' => 'drupal_group_css',
     '#aggregate_callback' => 'drupal_aggregate_css',
   );
+  $types['scripts'] = array(
+    '#items' => array(),
+    '#pre_render' => array('drupal_pre_render_scripts'),
+  );
 
   // Input elements.
   $types['submit'] = array(
